Create multiple objects in one form Django

Question:

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.

######## EDIT ##########

Following @nigel222’s answer, I have now created the following form:

class StudentSignUpForm(forms.ModelForm):
    
    class Meta:
        model = Student
        fields = (
            'username', 
            'email', 
            'first_name', 
            'last_name', 
            'year', 
            'school', 
            'student_email', 
            'account_status', 
            'phone_number', 
            'homework_coach', 
            'password1',
            'password2',
            'contact1_first_name', 
            'contact1_last_name', 
            'contact1_phone_number',
            'contact1_email',
            'contact1_street',
            'contact1_street_number',
            'contact1_zipcode',
            'contact1_city',
            'contact2_first_name',
            'contact2_last_name',
            'contact2_phone_number',
            'contact2_email',
            'contact2_street',
            'contact2_street_number',
            'contact2_zipcode',
            'contact2_city',
            )


    # user
    username = forms.CharField(label='Username', min_length=5, max_length=150)  
    email = forms.EmailField(label='Email')  
    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Confirm Password', widget=forms.PasswordInput)

    # contact 1
    contact1_first_name = forms.CharField(max_length=50, required=True)
    contact1_last_name = forms.CharField(max_length=50, required=True)
    contact1_phone_number = forms.CharField(max_length=50, required=False)
    contact1_email = forms.EmailField(required=False) # not required because some students might not know contact information
    contact1_street = forms.CharField(max_length=100, required=False)
    contact1_street_number = forms.CharField(max_length=10, required=False)
    contact1_zipcode = forms.CharField(max_length=10, required=False)
    contact1_city = forms.CharField(max_length=100, required=False)

    # contact 2
    contact2_first_name = forms.CharField(max_length=50, required=True)
    contact2_last_name = forms.CharField(max_length=50, required=True)
    contact2_phone_number = forms.CharField(max_length=50, required=False)
    contact2_email = forms.EmailField(required=False) # not required because some students might not know contact information
    contact2_street = forms.CharField(max_length=100, required=False)
    contact2_street_number = forms.CharField(max_length=10, required=False)
    contact2_zipcode = forms.CharField(max_length=10, required=False)
    contact2_city = forms.CharField(max_length=100, required=False)

    def username_clean(self):  
        username = self.cleaned_data['username'].lower()  
        new = User.objects.filter(username = username)  
        if new.count():  
            raise ValidationError("User Already Exist")  
        return username  
  
    def email_clean(self):  
        email = self.cleaned_data['email'].lower()  
        new = User.objects.filter(email=email)  
        if new.count():  
            raise ValidationError(" Email Already Exist")  
        return email  
  
    def clean_password2(self):  
        password1 = self.cleaned_data['password1']  
        password2 = self.cleaned_data['password2']  
  
        if password1 and password2 and password1 != password2:  
            raise ValidationError("Password don't match")  
        return password2  

    def save(self, commit = True):  
        user = User.objects.create_user(  
            self.cleaned_data['username'],  
            self.cleaned_data['email'],  
            self.cleaned_data['password1']  
        )  
        
        return user
    
    @transaction.atomic
    def form_valid(self, form):
        student = form.save()

        user = super().save(commit=False)
        user.is_student = True
        user.save()

        contact1 = Contact(
            student = student,
            contact_first_name = form.cleaned_data['contact1_first_name'],
            contact_last_name = form.cleaned_data['contact1_last_name'],
            contact_phone_number = form.cleaned_data['contact1_phone_number'],
            contact_email = form.cleaned_data['contact1_email'],
            contact_street = form.cleaned_data['contact1_street'],
            contact_street_number = form.cleaned_data['contact1_street_number'],
            contact_zipcode = form.cleaned_data['contact1_zipcode'],
            contact_city = form.cleaned_data['contact1_city'],
        )
        contact1.save()

        if (form.cleaned_data['contact2_first_name'] and 
            form.cleaned_data['contact2_last_name']  # blank if omitted
        ):

            contact2 = Contact(
                student=student,
                contact_first_name = form.cleaned_data['contact2_first_name'],
                contact_last_name = form.cleaned_data['contact2_last_name'],
                contact_phone_number = form.cleaned_data['contact2_phone_number'],
                contact_email = form.cleaned_data['contact2_email'],
                contact_street = form.cleaned_data['contact2_street'],
                contact_street_number = form.cleaned_data['contact2_street_number'],
                contact_zipcode = form.cleaned_data['contact2_zipcode'],
                contact_city = form.cleaned_data['contact2_city'],
            )
            contact2.save()

        return redirect('home')

And because the form is now a ModelForm, instead of a UserCreationForm, I had to add various functions to validate the user’s credentials, such as save(), clean_password2() and email_clean().

And created this view in views.py:

def student_signup(request):
    if request.method == 'POST':
        student_signup_form = StudentSignUpForm()
        if student_signup_form.is_valid():
            student_signup_form.form_valid(student_signup_form)
        return redirect('home')
    else:
        student_signup_form = StudentSignUpForm()
        return render(request, 'registration/signup_form.html', {'student_signup_form': student_signup_form})

The forms render perfectly now: there are fields for the User’s password and username, the Student’s name etc, and finally the two contacts, all in one form. So we’re getting there!

The only problem is that when I fill the form in, nothing is saved in the database after I hit submit. Does anyone know why this happens?

Asked By: HTTP ERROR 500

||

Answers:

What I’d try:

I don’t understand your StudentSignUpForm magic. However, if it’s effectively the same as a ModelForm:

class StudentSignUpForm(forms.Modelform):
    class Meta:
         model = Student
         fields = ('first_name', 'last_name', ...)

then just add non-model fields

    contact1_first_name = forms.CharField(max_length=50, required=True)
    contact1_last_name = forms.CharField(max_length=50, required=True)
    contact1_phone_number = forms.CharField(max_length=50, required=False)
    ...
    contact2_first_name = forms.CharField(max_length=50, required=True)
    ...
    contact2_zipcode = forms.CharField(max_length=10, required=False)
    contact2_city = forms.CharField(max_length=100, required=False)

And then put everything together in form_valid:

@transaction.atomic
def form_valid( self, form):
    student = form.save()

    contact1 = Contact(
        student = student,
        contact_first_name = form.cleaned_data['contact1_first_name'],
        contact_last_name =  ...
    )
    contact1.save()

    if (form.cleaned_data['contact2_first_name'] and 
        form.cleaned_data['contact2_last_name']  # blank if omitted
       ):

        contact2 = Contact(
            student=student,
            contact_first_name = form.cleaned_data['contact2_first_name'],
            ...
        )
        contact2.save()

    return HttpResponseRedirect( ...)

If you want to do further validation beyond what’s easy in a form definition you can. (You may well want to check that if conatct2_first_name is specified, contact2_last_name must also be specified).

def form_valid( self, form):

    # extra validations, add errors on fail

    n=0
    if form.cleaned_data['contact2_first_name']:
       n+=1
    if form.cleaned_data['contact2_last_name']:
       n+=1

    if n==1:
       form.add_error('contact2_first_name',
          'Must provide first and last names for contact2, or omit both for no second contact') 
       form.add_error('contact2_last_name',
          'Must provide first and last names for contact2, or omit both for no second contact') 
    contact2_provided = (n != 0)
    ...

    if not form.is_valid():
       return self.form_invalid( self, form)

    with transaction.atomic():
        student = form.save()
        contact1 = ( ... # as before

        if contact2_provided:
           contact2 = ( ...
Answered By: nigel222

Thanks to @nigel222 I have come a long way in creating the form. However, the form did not save the data to the database properly, but I managed to change the view and the form a little bit and it works now.

This is a blueprint on how to create two objects in one form in Django. It creates one Student model (linked to a User model) and two Contact models (of which one is optional).

Forms.py:

class StudentSignUpForm(forms.ModelForm):
    
    class Meta:
        model = Student
        fields = (
            'username',  
            'first_name', 
            'last_name', 
            'year', 
            'school', 
            'student_email', 
            'account_status', 
            'phone_number', 
            'homework_coach', 
            'password1',
            'password2',
            'contact1_first_name', 
            'contact1_last_name', 
            'contact1_phone_number',
            'contact1_email',
            'contact1_street',
            'contact1_street_number',
            'contact1_zipcode',
            'contact1_city',
            'contact2_first_name',
            'contact2_last_name',
            'contact2_phone_number',
            'contact2_email',
            'contact2_street',
            'contact2_street_number',
            'contact2_zipcode',
            'contact2_city',
            )

    # user
    username = forms.CharField(label='Username', min_length=5, max_length=150)   
    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Confirm Password', widget=forms.PasswordInput)

    # contact 1
    contact1_first_name = forms.CharField(max_length=50, required=True)
    contact1_last_name = forms.CharField(max_length=50, required=True)
    contact1_phone_number = forms.CharField(max_length=50, required=False)
    contact1_email = forms.EmailField(required=False) # not required because some students might not know contact information
    contact1_street = forms.CharField(max_length=100, required=False)
    contact1_street_number = forms.CharField(max_length=10, required=False)
    contact1_zipcode = forms.CharField(max_length=10, required=False)
    contact1_city = forms.CharField(max_length=100, required=False)

    # contact 2
    contact2_first_name = forms.CharField(max_length=50, required=False) # not required because second contact is optional
    contact2_last_name = forms.CharField(max_length=50, required=False)
    contact2_phone_number = forms.CharField(max_length=50, required=False)
    contact2_email = forms.EmailField(required=False) # not required because some students might not know contact information
    contact2_street = forms.CharField(max_length=100, required=False)
    contact2_street_number = forms.CharField(max_length=10, required=False)
    contact2_zipcode = forms.CharField(max_length=10, required=False)
    contact2_city = forms.CharField(max_length=100, required=False)

    def username_clean(self):  
        username = self.cleaned_data['username'].lower()  
        new = User.objects.filter(username = username)  
        if new.count():  
            raise ValidationError("User Already Exist")  
        return username  
  
    def clean_password2(self):  
        password1 = self.cleaned_data['password1']  
        password2 = self.cleaned_data['password2']  
  
        if password1 and password2 and password1 != password2:  
            raise ValidationError("Password don't match")  
        return password2  

    @transaction.atomic
    def form_valid(self, form):
    
        user = User.objects.create_user(  
            self.cleaned_data['username'],  
            self.cleaned_data['password1']  
        ) 
        user.is_student = True
        user.save()

        student = form.save(commit=False)
        student.user = user
        student = form.save()

        contact1 = Contact(
            student = student,
            contact_first_name = form.cleaned_data['contact1_first_name'],
            contact_last_name = form.cleaned_data['contact1_last_name'],
            contact_phone_number = form.cleaned_data['contact1_phone_number'],
            contact_email = form.cleaned_data['contact1_email'],
            contact_street = form.cleaned_data['contact1_street'],
            contact_street_number = form.cleaned_data['contact1_street_number'],
            contact_zipcode = form.cleaned_data['contact1_zipcode'],
            contact_city = form.cleaned_data['contact1_city'],
        )
        contact1.save()

        if (form.cleaned_data['contact2_first_name'] and 
            form.cleaned_data['contact2_last_name']  # blank if omitted
        ):

            contact2 = Contact(
                student=student,
                contact_first_name = form.cleaned_data['contact2_first_name'],
                contact_last_name = form.cleaned_data['contact2_last_name'],
                contact_phone_number = form.cleaned_data['contact2_phone_number'],
                contact_email = form.cleaned_data['contact2_email'],
                contact_street = form.cleaned_data['contact2_street'],
                contact_street_number = form.cleaned_data['contact2_street_number'],
                contact_zipcode = form.cleaned_data['contact2_zipcode'],
                contact_city = form.cleaned_data['contact2_city'],
            )
            contact2.save()

        return redirect('home')

Models.py:

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, blank=True, default='')
    school = models.ForeignKey(School, on_delete=models.SET_NULL, null=True, blank=True, default='')
    student_email = models.EmailField() # named student_email because email conflicts with user email
    account_status = models.CharField(max_length=1, choices=ACCOUNT_STATUS_CHOICES, default='A')
    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}"

Urls.py:

urlpatters = [
    path('signup/student/', views.student_signup, name='student_signup'),
]

Views.py:

def student_signup(request):
    if request.method == 'POST':
        student_signup_form = StudentSignUpForm(request.POST)
        if student_signup_form.is_valid():
            student_signup_form.form_valid(student_signup_form)
            return redirect('home')
        else:
            return HttpResponse("Form is not valid")
    else:
        student_signup_form = StudentSignUpForm()
        return render(request, 'registration/signup_form.html', {'student_signup_form': student_signup_form})
Answered By: HTTP ERROR 500