You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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中,但提交表单后数据未保存到数据库。

问题原因及修正方案

你提交后数据未保存的核心原因有以下几点:

  1. POST请求未传入表单数据:原视图中student_signup_form = StudentSignUpForm()没有传入request.POST,导致表单无法获取提交的数据,自然无法通过验证和保存。
  2. 表单Meta字段错误:原表单的Meta.fields包含了大量不属于Student模型的字段(如username、email、password1等),这会导致ModelForm初始化异常,无法正确处理数据。
  3. 表单方法定义错误:原表单中定义了form_valid方法,这是类视图的方法,不应该放在表单类中,正确的做法是在表单的save方法中处理所有对象的创建逻辑。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 16:40:22