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.