Django单表单创建Student与可选Contact对象:提交数据不保存排查
Django单表单创建Student及可选双Contact对象问题
需求
我需要在Django中实现一个表单,提交后可同时创建1个Student对象和2个Contact对象,其中第二个Contact为非必填项。对象关系如下:
Contact 1 Student < Contact 2 (非必填)
现有模型定义(models.py)
from django.contrib.auth.models import AbstractUser from django.db import models 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() # 命名为student_email避免与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}"
初始实现代码
表单(forms.py)
from django import forms from django.contrib.auth.forms import UserCreationForm from django.db import transaction from .models import Student, Teacher, User, Year, School, 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) # 新学校/年级未入库时可选填 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( # 创建Student对象 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) # 部分学生可能不知道联系人信息 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__'
视图(views.py)
from django.contrib.auth import login from django.views.generic.edit import CreateView from django.shortcuts import redirect from .models import User from .forms import StudentSignUpForm 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') # 此处存在错误,应获取homework_coach字段 user = form.save( 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')
模板(registration/signup_form.html)
{% 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 %}
URL配置(urls.py)
from django.urls import path from .views import StudentSignUpView, views urlpatterns = [ path('', views.home, name='home'), path('signup/student/', StudentSignUpView.as_view(), name='student_signup'), ]
问题与尝试
核心问题
如何实现一个视图,通过单个表单完成1个Student、2个Contact(第二个可选)的创建?
已尝试方案
我尝试用formset批量创建Contact,但只能生成多个Contact对象,无法整合Student字段。代码如下:
from django.forms import formset_factory from django.shortcuts import render from .forms import ContactForm def formset_view(request): context={} # 创建formset ContactFormSet = formset_factory(ContactForm, extra = 2) formset = ContactFormSet() # 验证通过则打印数据 if formset.is_valid(): for form in formset: print(form.cleaned_data) context['formset']=formset return render(request, 'registration/signup_form.html', context)
修改后的URL配置:
from django.urls import path from .views import formset_view, views urlpatterns = [ path('', views.home, name='home'), path('signup/student/', views.formset_view, name='student_signup'), ]
但该方案仅能处理Contact对象,无法同时创建Student,尝试用ModelFormSet添加Student字段也未成功。
编辑更新:改进后的代码及新问题
参考建议后,我将所有字段整合到一个ModelForm中,但提交表单后数据未保存到数据库。
问题原因及修正方案
你提交后数据未保存的核心原因有以下几点:
- POST请求未传入表单数据:原视图中
student_signup_form = StudentSignUpForm()没有传入request.POST,导致表单无法获取提交的数据,自然无法通过验证和保存。 - 表单Meta字段错误:原表单的
Meta.fields包含了大量不属于Student模型的字段(如username、email、password1等),这会导致ModelForm初始化异常,无法正确处理数据。 - 表单方法定义错误:原表单中定义了
form_valid方法,这是类视图的方法,不应该放在表单类中,正确的做法是在表单的save方法中处理所有对象的创建逻辑。 - Contact 2的必填设置错误:原表单中把
contact2_first_name和contact2_last_name设为required=True,但需求是第二个Contact可选,应该改为required=False。
以下是修复后的完整代码:
修正后的表单(forms.py)
from django import forms from django.contrib.auth.models import User from django.db import transaction from .models import Student, Contact class StudentSignUpForm(forms.ModelForm): class Meta: model = Student fields = ( 'first_name', 'last_name', 'year', 'school', 'student_email', 'account_status', 'phone_number', 'homework_coach', ) # User字段 username = forms.CharField(label='用户名', min_length=5, max_length=150) email = forms.EmailField(label='邮箱') password1 = forms.CharField(label='密码', widget=forms.PasswordInput) password2 = forms.CharField(label='确认密码', widget=forms.PasswordInput) # Contact 1字段 contact1_first_name = forms.CharField(max_length=50, required=True, label='联系人1姓名') contact1_last_name = forms.CharField(max_length=50, required=True, label='联系人1姓氏') contact1_phone_number = forms.CharField(max_length=50, required=False, label='联系人1电话') contact1_email = forms.EmailField(required=False, label='联系人1邮箱') contact1_street = forms.CharField(max_length=100, required=False, label='联系人1街道') contact1_street_number = forms.CharField(max_length=10, required=False, label='联系人1门牌号') contact1_zipcode = forms.CharField(max_length=10, required=False, label='联系人1邮编') contact1_city = forms.CharField(max_length=100, required=False, label='联系人1城市') # Contact 2字段(非必填) contact2_first_name = forms.CharField(max_length=50, required=False, label='联系人2姓名') contact2_last_name = forms.CharField(max_length=50, required=False, label='联系人2姓氏') contact2_phone_number = forms.CharField(max_length=50, required=False, label='联系人2电话') contact2_email = forms.EmailField(required=False, label='联系人2邮箱') contact2_street = forms.CharField(max_length=100, required=False, label='联系人2街道') contact2_street_number = forms.CharField(max_length=10, required=False, label='联系人2门牌号') contact2_zipcode = forms.CharField(max_length=10, required=False, label='联系人2邮编') contact2_city = forms.CharField(max_length=100, required=False, label='联系人2城市') def clean_username(self): username = self.cleaned_data['username'].lower() if User.objects.filter(username=username).exists(): raise forms.ValidationError("该用户名已存在") return username def clean_email(self): email = self.cleaned_data['email'].lower() if User.objects.filter(email=email).exists(): raise forms.ValidationError("该邮箱已被注册") return email def clean_password2(self): password1 = self.cleaned_data.get('password1') password2 = self.cleaned_data.get('password2') if password1 and password2 and password1 != password2: raise forms.ValidationError("两次输入的密码不一致") return password2 @transaction.atomic def save(self, commit=True): # 创建User user = User.objects.create_user( username=self.cleaned_data['username'], email=self.cleaned_data['email'], password=self.cleaned_data['password1'] ) user.is_student = True user.save() # 创建Student student = super().save(commit=False) student.user = user if commit: student.save() # 创建Contact 1 Contact.objects.create( student=student, contact_first_name=self.cleaned_data['contact1_first_name'], contact_last_name=self.cleaned_data['contact1_last_name'], contact_phone_number=self.cleaned_data['contact1_phone_number'], contact_email=self.cleaned_data['contact1_email'], contact_street=self.cleaned_data['contact1_street'], contact_street_number=self.cleaned_data['contact1_street_number'], contact_zipcode=self.cleaned_data['contact1_zipcode'], contact_city=self.cleaned_data['contact1_city'], ) # 创建Contact 2(仅当必填字段有值时) contact2_first = self.cleaned_data.get('contact2_first_name') contact2_last = self.cleaned_data.get('contact2_last_name') if contact2_first and contact2_last: Contact.objects.create( student=student, contact_first_name=contact2_first, contact_last_name=contact2_last, contact_phone_number=self.cleaned_data['contact2_phone_number'], contact_email=self.cleaned_data['contact2_email'], contact_street=self.cleaned_data['contact2_street'], contact_street_number=self.cleaned_data['contact2_street_number'], contact_zipcode=self.cleaned_data['contact2_zipcode'], contact_city=self.cleaned_data['contact2_city'], ) return student
修正后的视图(views.py)
from django.contrib.auth import login from django.shortcuts import render, redirect from .forms import StudentSignUpForm def student_signup(request): if request.method == 'POST': form = StudentSignUpForm(request.POST) if form.is_valid(): student = form.save() login(request, student.user) # 登录用户 return redirect('home') # 若表单验证失败,重新渲染表单并显示错误 return render(request, 'registration/signup_form.html', {'form': form}) else: form = StudentSignUpForm() return render(request, 'registration/signup_form.html', {'form': form})
修正后的模板(registration/signup_form.html)
{% block content %} {% load crispy_forms_tags %} <form method="POST" enctype="multipart/form-data"> {% csrf_token %} {{ form|crispy }} <input type="submit" value="提交"> </form> {% endblock %}
内容的提问来源于stack exchange,提问作者HTTP ERROR 500
相关产品推荐
相关产品推荐

