Создание нескольких объектов в одной форме Django

Я пытаюсь создать форму в Django, которая может создавать один объект Student с двумя объектами Contact в одной форме. Второй объект Contact должен быть необязательным для заполнения (не обязательным).

Схематическое представление объектов, созданных в единой форме:

          Contact 1
Student <
          Contact 2 (not required)

У меня есть следующие модели в 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}"

В файле forms.py я создал две формы для регистрации студентов и контактов. Студент также подключен к объекту User для входа и аутентификации, но это не имеет значения. Следовательно, когда создается пользователь, он определяется как 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__'
        

В файле views.py я создал представление, которое сохраняет данные (пока только данные студента, не контактные данные).

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')

А в файле 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 %}

Urls.py:

from .views import StudentSignUpView

urlpatterns = [
    path('', views.home, name='home'),
    path('signup/student/', StudentSignupView.as_view(), name='student_signup'),
]

Как я могу создать одно представление, которое имеет одну форму, создающую 1 объект Student и 2 объекта Contact (из которых 2-й контакт не обязателен)?

То, что я пробовал:

Использование наборов форм для создания нескольких контактов одновременно, но мне удалось создать только несколько Контактов и не удалось добавить Студентов в этот набор форм.

Я добавил это в 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'),
]

Но мне удалось создать только несколько контактов и не удалось добавить объект Student через эту форму. Я попробовал создать ModelFormSet для добавления полей для объекта Student, но и это не сработало.

Что бы я попробовал:

Я не понимаю вашей магии StudentSignUpForm. Однако, если это фактически то же самое, что и ModelForm:

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

тогда просто добавьте немодельные поля

    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)

А затем соберите все вместе в 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( ...)

Если вы хотите сделать дополнительную проверку, помимо той, что легко выполняется в определении формы, вы можете:

def form_valid( self, form):

    # extra validations, add errors on fail
    if (something based on form.cleaned_data[ ...]):
        form.add_error( 'field_name', 'error_message')
    if ( ...)
        form.add_error( ...)

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

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

Благодаря @nigel222 я прошел долгий путь в создании формы. Однако форма не сохраняла данные в базу данных должным образом, но мне удалось немного изменить представление и форму, и теперь она работает.

Это чертеж о том, как создать два объекта в одной форме в Django. Он создает одну модель Student (связанную с моделью User) и две модели Contact (одна из которых является необязательной).

Forms.py:

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})
Вернуться на верх