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?
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 = ( ...