Saving multiple optional objects with a CreateView

Hey everyone,
I’m working on my first real application and I’m having trouble grasping the concept of saving multiple objects and tying them together. I’m making a real estate app where users can apply to rent a house or apartment. I want a form to allow 1 to 4 people to apply for a single property. I either want 1 page where the user can select the number of applicants and the form generates x number of person forms for the users to fill out, or I want to add 2 buttons to save and complete or save and continue to generate the form again. Either option will work for me I’m just not sure how I will tie them all together in my application table.

models
class Properties(models.Model):
property_num = models.IntegerField()
property_road = models.CharField(max_length=200)
rent = models.IntegerField(default=1000)
city = models.CharField(max_length=200)
state = models.CharField(choices=STATE_CHOICE, max_length=25)
zip = models.IntegerField()
bedrooms = models.IntegerField()
bathrooms = models.FloatField(max_length=3)
sq_foot = models.IntegerField()
description = models.TextField(default=bedrooms)
is_active = models.BooleanField(default=True)
thumbnail = models.ImageField(upload_to=‘static/images’, blank=True)
slug = models.SlugField(blank=True, null=True, unique=True, allow_unicode=True)

class Person(models.Model):
address = models.ForeignKey(Properties, on_delete=models.CASCADE)
requested_move_in_date = models.DateField(default=datetime.now)
first_name = models.CharField(max_length=200)
middle_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
dob = models.CharField(max_length=200)
driver_license = models.IntegerField()
driver_license_state = models.CharField(choices=STATE_CHOICE, max_length=25)

class Application(models.Model):
applicant1 = models.ForeignKey(Person, related_name=‘applicant’, on_delete=models.CASCADE)
applicant2 = models.ForeignKey(Person, related_name=‘co_applicant1’, blank=True, null=True, on_delete=models.CASCADE)
applicant3 = models.ForeignKey(Person, related_name=‘co_applicant2’, blank=True, null=True, on_delete=models.CASCADE)
applicant4 = models.ForeignKey(Person, related_name=‘co_applicant3’, blank=True, null=True, on_delete=models.CASCADE)
property = models.ForeignKey(Properties, on_delete=models.CASCADE)
application_date = models.DateTimeField(default=datetime.now)

Form
class ApplicationForm(forms.ModelForm):
class Meta:
model = Person
fields = ‘all
# exclude = [‘application_date’]
widgets = {
‘job_start_date’: DateInput(),
‘current_address_end_date’: DateInput(),
‘current_address_start_date’: DateInput(),
‘previous_address_end_date’: DateInput(),
‘previous_address_start_date’: DateInput(),
‘application_date’: forms.HiddenInput(),

View
class ApplicationView(CreateView):
form_class = ApplicationForm
model = Person
template_name = ‘base/application.html’
redirect_field_name = ‘Thankyou’

This type of question raises all sorts of questions regarding exactly what it is you’re trying to model and what the longer-term on-going data requires.

What you need to determine as part of your “system requirements” are the overall objectives of this system. Are you just replacing some “pen and paper” forms? Or is this part of (potentially) a larger system? What are you looking to do with this data after collecting it?

Before creating any code, you should have a solid understanding of the data being managed - the entities (Person, Application, Property), their relationships with each other, and how those relationships are managed over time.
(Now, that’s not to say that these requirements can’t change - they absolutely can. Nothing is etched in stone. But you do need to know what your starting position is going to be.)

Just to give you an idea of some of the questions raised -

  • Do you want to be able to have the same person able to apply for multiple properties?

    • Or can one individual only apply for one property?
    • Or can they only apply for one property at a time?
      (in other words, can they apply for property “X”, and then at a later time apply for property “Y”?)
  • If a person applies for property “X” and then later applies for property “Y”, are you looking to reuse the existing Person for that person, or create a new instance for that application?

Different answers to these questions are likely to yield different model structures.

(Side note: You’ve defined your Person and Application model using a singular name, which is good, but you’ve got Properties defined as a plural. Suggest you change it to Property.)

Thanks for responding so quickly Ken. Over the last week, a lot of my original ideas have been changing as I get further along. Let me first start off with my reason and goals for this project. I started this project because I wanted something to give me more practice with Django and to learn new skills with it and to have some real projects for my portfolio. My goal was to make a simple app to showcase my rentals and to replace pen and paper applications so I can screen out potential tenants.
My goal is at a later date, to add user accounts because I want to add in a payment option to collect fees for background checks, credit checks, and rent ( I use a 3rd party for this now). However, I may just end up doing that now to save myself a future headache.

To answer your questions

A user can apply for whatever property is available at a given time. At my current scale, it’s unlikely that any one person would be applying for several properties at once. I feel like this can easily be achieved by using user accounts if I go that route

I do want to do this but I feel like this comes into play when I enable user accounts. Please correct me if I’m wrong and there is a way to it without user accounts

That’s all really helpful. But you kind of threw me a curveball there by making references to something you’re referring to as “User Accounts”.

If by “User Accounts” you’re just referring to people being represented by an instance of the User model, then no, it’s not necessary to do that. In essence, you could think of the Person model as your application’s “User”.

But, if you do want to eventually provide the option for every person to be able to log on as their own account, then “Person” could be a profile model associated with a User. (If you even think you might want to go in this direction, it’s significantly easier to start with it set up this way. Keep in mind that there’s nothing requiring those User accounts to be “active” or able to log in, just that they exist.)

I do have a number of thoughts on your basic design - unfortunately my time available right now doesn’t isn’t sufficient for that. I’m going to have to defer a more appropriate answer for a few hours.

I was referring to giving users an option to register for an account on the website. I feel like when I want to start collecting payments that I’ll need to have that setup so I believe you are right. Might as well set it up now while I’m at this point.

Data modeling isn’t my strong point. So any recommendations I will greatly appreciate. I’ve only got 1 out of date Django course from Udemy under my belt so any other Django tips you can give me would be awesome too

What follows is just one person’s opinion. This is only one solution, there are other, equally valid, solutions for situations like this.

In general, you have a Property model. No comments on that, I’ll assume you know that domain much better than I.

You have a Person model. I’d remove the FK to Property (I’ll address that later) and would add a OneToOne field to User. This creates the profile relationship to the User model. It does add the requirement that when a Person is created, you will also need to create the corresponding User model or otherwise ensure it exists. (You can create that instance with the is_active attribute False if you’re not interested in allowing them to log on at this time.)

Then you have this Application model. Here’s where things get hairy. You have a Many-to-many relationship between Property and Person, with the Application acting as a “through” model. However, the application itself serves as a logical group with up to 4 Person on that application. And, there is the theoretical possibility that a person could be part of two separate groups filling out an application for a property.

This leads me to creating something like:

class Application(...):
    property = ForeignKey(Property)
    application_date = DateField()

class Applicant(...):
    person = ForeignKey(Person)
    application = ForeignKey(Application)

Where the Applicant model contain constraints to ensure:

  • The combination of (application, person) is unique
  • There are no more than 4 instances of person for an application in Applicant

This is a more normalized structure for this relationship. If you need to identify an ordering to the Applicants of an Application, you can add a sequence field to Applicant.

I decided to just go ahead and implement full-on user accounts with registration, login, and profiles to avoid having to do a major update later. So if I’m understanding you correctly. In my Person model add
user = models.OneToOneField(User, on_delete=models.CASCADE)? When I made this post I shortened my Person model to avoid a large wall of text. Should I split up my person model? I feel like I should keep my person model limited to first block (name through email) and put the rest in a different model.

class Person(models.Model):
    address = models.ForeignKey(Properties, on_delete=models.CASCADE)
    requested_move_in_date = models.DateField(default=datetime.now)
    first_name = models.CharField(max_length=200)
    middle_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)
    dob = models.CharField(max_length=200)
    driver_license = models.IntegerField()
    driver_license_state = models.CharField(choices=STATE_CHOICE, max_length=25)
    ssi = models.IntegerField()
    phone = models.IntegerField()
    email = models.EmailField(max_length=200)

    current_address = models.CharField(max_length=200)
    current_address_start_date = models.DateField(max_length=10)
    current_address_end_date = models.DateTimeField(blank=True, null=True)
    current_rent = models.IntegerField()
    current_city = models.CharField(max_length=200)
    current_state = models.CharField(choices=STATE_CHOICE, max_length=15)
    current_zip = models.IntegerField()
    current_landlord = models.CharField(max_length=200)
    current_landlord_phone = models.IntegerField()
    current_reason_for_moving = models.TextField(max_length=200)

    previous_address = models.CharField(max_length=200, blank=True, null=True)
    previous_address_start_date = models.DateTimeField(blank=True, null=True)
    previous_address_end_date = models.DateTimeField(blank=True, null=True)
    previous_rent = models.IntegerField(blank=True, null=True)
    previous_city = models.CharField(max_length=200, blank=True, null=True)
    previous_state = models.CharField(choices=STATE_CHOICE, max_length=15, blank=True, null=True)
    previous_zip = models.IntegerField(blank=True, null=True)
    previous_landlord = models.CharField(max_length=200, blank=True, null=True)
    previous_landlord_phone = models.IntegerField(blank=True, null=True)
    previous_reason_for_moving = models.TextField(max_length=200, blank=True, null=True)
    pets = models.CharField(choices=PET_CHOICES, max_length=3)
    number_of_pets = models.IntegerField(blank=True, null=True)
    employer = models.CharField(max_length=200)
    employer_address = models.CharField(max_length=200)
    employer_city = models.CharField(max_length=200)
    employer_state = models.CharField(choices=STATE_CHOICE, max_length=25)
    employer_zip = models.IntegerField()
    job_title = models.CharField(max_length=200)
    job_start_date = models.DateTimeField()
    supervisor_name = models.CharField(max_length=200)
    gross_income = models.IntegerField()
    # application_date = models.DateTimeField(default=timezone.now)
    additional_comments = models.TextField(max_length=250, blank=True)

I made a new app just to visualize the models and data that you recommended and I think I have a better understanding of what your idea is. Is this what you are referring to?

class Property(models.Model):
    address = models.CharField(max_length=200)

    def __str__(self):
        return self.address

class Person(models.Model):
    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name

class Application(models.Model):
    property = models.ForeignKey(Property, on_delete=models.CASCADE)
    application_date = models.DateField()

    def __str__(self):
        return "{}".format(self.property)


class Applicant(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    application = models.ForeignKey(Application, on_delete=models.CASCADE)
    class Meta:
        unique_together = [['person', 'application']]

image
image

How do limit the max number of Persons per application?

Yep, pretty much right on track with what I was thinking.

Now for more comments from the peanut gallery…

class Person(models.Model):
    address = models.ForeignKey(Properties, on_delete=models.CASCADE)
    requested_move_in_date = models.DateField(default=datetime.now)
  • You don’t need an address and requested_move_in_date on the Person - those are attributes of the Application.
    first_name = models.CharField(max_length=200)
    middle_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)
    dob = models.CharField(max_length=200)
    driver_license = models.IntegerField()
    driver_license_state = models.CharField(choices=STATE_CHOICE, max_length=25)
    ssi = models.IntegerField()
    phone = models.IntegerField()
    email = models.EmailField(max_length=200)

These are all good for the Person object.

    current_address = models.CharField(max_length=200)
    current_address_start_date = models.DateField(max_length=10)
    current_address_end_date = models.DateTimeField(blank=True, null=True)
    current_rent = models.IntegerField()
    current_city = models.CharField(max_length=200)
    current_state = models.CharField(choices=STATE_CHOICE, max_length=15)
    current_zip = models.IntegerField()
    current_landlord = models.CharField(max_length=200)
    current_landlord_phone = models.IntegerField()
    current_reason_for_moving = models.TextField(max_length=200)

    previous_address = models.CharField(max_length=200, blank=True, null=True)
    previous_address_start_date = models.DateTimeField(blank=True, null=True)
    previous_address_end_date = models.DateTimeField(blank=True, null=True)
    previous_rent = models.IntegerField(blank=True, null=True)
    previous_city = models.CharField(max_length=200, blank=True, null=True)
    previous_state = models.CharField(choices=STATE_CHOICE, max_length=15, blank=True, null=True)
    previous_zip = models.IntegerField(blank=True, null=True)
    previous_landlord = models.CharField(max_length=200, blank=True, null=True)
    previous_landlord_phone = models.IntegerField(blank=True, null=True)
    previous_reason_for_moving = models.TextField(max_length=200, blank=True, null=True)

I’d think about creating this as an “Address” model for just one set of data, allowing for two instances to be assigned to the User, named “current” and “previous”.

    pets = models.CharField(choices=PET_CHOICES, max_length=3)
    number_of_pets = models.IntegerField(blank=True, null=True)

These probably belong in the Person model

    employer = models.CharField(max_length=200)
    employer_address = models.CharField(max_length=200)
    employer_city = models.CharField(max_length=200)
    employer_state = models.CharField(choices=STATE_CHOICE, max_length=25)
    employer_zip = models.IntegerField()
    job_title = models.CharField(max_length=200)
    job_start_date = models.DateTimeField()
    supervisor_name = models.CharField(max_length=200)
    gross_income = models.IntegerField()
    # application_date = models.DateTimeField(default=timezone.now)
    additional_comments = models.TextField(max_length=250, blank=True)

I’d break this out into an Employment model, relating to User.
(Note: When I say “relating to”, you can read that as saying “with a ForeignKey field referencing”)

I would suggest using an inlineformset as part of the Application form, where the formset is for Applicant. Limit the formset to 4 forms.

(Personal note: I found formsets to be one of the most confusing parts of Django. They took me a long time to wrap my head around them to get to the point where I felt reasonably comfortable using them. They’re powerful and very useful. Even so, I’m probably still only using about 50% of the features available with them. I suggest you not be surprised if you find them frustrating at first.)

I like all those ideas but this brings up my original issue. How do I write to multiple models on a single form? Right now, I’m using a CreateView to generate my form and write to my model. Is there a way to use a CreateView with multiple models? I probably have to use a FBV and just write it all out manually? If that’s the case do you have an example? If that is the case then how do I pass my objects around to each view so I can associate the data? Do you have an example you can point me to? That will help me visualize the process.

Not easily - or I should say, not that appears to me to save you a whole lot of effort.

The Django-provided generic CBVs work best with single-model forms. They aren’t really designed to work with multiple models.

(Side note: The Django-provided generic CBVs are not the only possible CBVs. You could - if you had a desire to do so - create a set of CBVs that satisfied this use-case. I’ve never encountered a situation where I would have considered this to be desirable, but YMMV.)

That’s my recommendation.

Handling multiple models in a form isn’t fundamentally any different than handling 1, other than you’re doing things twice. (Note: You will want to use a prefix for each form instance.)

As a trivial example, taking as an excerpt from Working with forms | Django documentation | Django as a starting point, working with two forms is going to take the general shape of:

def get_name(request):
    if request.method == 'POST':
        form1 = NameForm(request.POST, prefix='name')
        form2 = PersonForm(request.POST, prefix='person')
        if form1.is_valid() and form2.is_valid():
            # ...
            return HttpResponseRedirect('/thanks/')
    else:
        form1 = NameForm(prefix='name')
        form2 = PersonForm(prefix='person')
    return render(request, 'name.html', {'form1': form1, 'form2': form2})

One aspect of this that I would suggest as a “mindset”, is that you pick one model as being the “base” model for the page - one that all other models relate to in some manner. That gives you your hook to find those related models to that “base” model.
(The specifics of that will depend upon how you want your page to function.)

I’ve rewritten my view to a FBV and updated template and I’m running into a few issues. I can now generate my forms but when I try to post I’m getting an error that person is required and I’m not sure how I set a Person in the template.

def Application_View(request):
    if request.method == 'POST':
        person = PersonForm(request.POST, prefix='person')
        current_address = AddressForm(request.POST, prefix='current_address')
        previous_address = AddressForm(request.POST, prefix='previous_address')
        employment = EmploymentForm(request.POST, prefix='employment')
        print("1 \n Person: {}\n Current Address: {}\n Previous Address: {}\n Employment: {}".format(person.errors, current_address.errors, previous_address.errors,employment.errors))
        if person.is_valid() and current_address.is_valid() and previous_address.is_valid() and employment.is_valid():
            person.save()
            current_address.person = person.auto_id
            current_address.save()
            previous_address.person = person.auto_id
            previous_address.save()
            employment.person = person.auto_id
            employment.save()
            return HttpResponseRedirect('/thanks/')
        else:
            print("i am not valid")
    else:
        person = PersonForm(prefix='person')
        current_address = AddressForm(prefix='current_address')
        previous_address = AddressForm(prefix='previous_address')
        employment = EmploymentForm(prefix='employment')
    return render(request, 'base/application.html', {'person': person,
                                                     'current_address': current_address,
                                                     'previous_address': previous_address,
                                                     'employment': employment
                                                     })
1 
 Person: 
 Current Address: <ul class="errorlist"><li>person<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
 Previous Address: <ul class="errorlist"><li>person<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
 Employment: <ul class="errorlist"><li>person<ul class="errorlist"><li>This field is required.</li></ul></li></ul>

Another issue I’m having now is with my address model. I originally had it set up that the previous address was optional, however, since I’m using the new Address model everything is required on both instances.

For clarity, I’d change this (and the similar cases below it) to be:
person_form = PersonForm(request.POST, prefix='person')
because this is a form and not a Person.

Then, you can write:
person = person_form.save()
(See Creating forms from models | Django documentation | Django)

Then, since you’re needing to modify the other objects being created, you want to use a sequence similar to what’s in the docs:

current_address_form = AddressForm(request.POST, prefix='current_address')
current_address = current_address_form.save(commit=False)
current_address.person = person
current_address.save()

Then separate your handling of the forms into multiple stages within the view.
You could do something like:

...
if person_form.is_valid():
    # Do what you need to do with the person form
    if current_address_form.is_valid():
        # Save the current address
    if previous_address_form.is_valid():
        # Save the previous address
   # Continue with anything else needing to be done if the person_form is valid
else:
    # person_form is not valid
...

This is where you need to think about exactly what you want to have happen under every combination of circumstances. For example, do you want to save the previous_address if current_address is not valid? If not, then this logic needs to change.
Or, you may wish to consider breaking this apart into separate pages - that would make some of the logic easier to handle and to “reason about”.

I’m still not able to associate my other forms to the Person. My person is saving flawlessly every time. The issue I’m running into is setting my address to the Person.

    if request.method == 'POST':
        person_form = PersonForm(request.POST, prefix='person')
        current_address_form = AddressForm(request.POST, prefix='current_address')
        previous_address_form = AddressForm(request.POST, prefix='previous_address')
        employment_form = EmploymentForm(request.POST, prefix='employment')

        print("1 \n Person: {}\n Current Address: {}\n Previous Address: {}\n Employment: {}".format(person_form.errors, current_address_form.errors, previous_address_form.errors,employment_form.errors))
        if person_form.is_valid():
            person = person_form.save()
            current_address_form.person = person

            print(current_address_form)

            if current_address_form.is_valid():
                current_address_form.save()
1 
 Person: 
 Current Address: <ul class="errorlist"><li>person<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
 Previous Address: <ul class="errorlist"><li>person<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
 Employment: <ul class="errorlist"><li>person<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
<tr>
    <th><label for="id_current_address-person">Person:</label></th>
    <td>
      <ul class="errorlist"><li>This field is required.</li></ul>
      <select name="current_address-person" required id="id_current_address-person">
  <option value="" selected>---------</option>

  <option value="1">Test User1</option>

  <option value="2">Test User2</option>

  <option value="3">Test User3</option>

  <option value="4">Test User4</option>

No matter what I try <option value="" selected>---------</option> is always empty

Yes, because this pattern of code doesn’t follow the pattern of code presented previously:

Yes, there may be other lines of code interspersed among this set of four lines, but these four lines must at some point be executed in this sequence.
(Most notably, that first line creating the current_address_form can be done before any of the is_valid tests. However, the last three are generally performed in this order within the true conditional of the corresponding is_valid.)

I tried doing it the way you mentioned previously and I was still unsuccessful. So if I’m understanding you correctly I should do it like this?

        if person_form.is_valid():
            person = person_form.save()
            if current_address_form.is_valid():
                current_address = current_address_form.save(commit=False)
                current_address.person = person
                current_address.save()

If this is the case how is is_valid() true? Won’t it always going to fail because current_address_form doesn’t have a Person yet? If I try to set the Person outside of is_valid() then current_address = current_address_form.save(commit=False) fails due to not validating the data prior to saving. This is what led me to the previous example.

The address form should be valid because the current_address.person shouldn’t be in the current_address form, and so it not being sent wouldn’t matter. (You don’t include fields in model forms that are not being set by user input.
(You haven’t posted your current_address form, so I don’t know whether this is the case or not.)

does not actually attempt to save to the database - as the docs show, this is exactly what you want to do to modify an instance being created by a form before it gets written to the database.

There’s my problem. I forgot to exclude the FKs on my forms

With all the information you have given me I have completed my forms. I got everything linking together (people to applicants and applicants to applications). I started working on a page to view my applications since my original idea is no longer feasible. I made a simple ListView to display all my current applications (I want to add filtering in later but that’s not a big deal right now this second). I’m having issues getting my Person’s Address and Employment data to my HTML page. I cannot seem to figure out how pull my Person’s ID to query the Address and Employment tables. Additionally, each Person may have multiple addresses so I’m not sure how I would tie that all together on the HTML side when I pass it to my HTML.

This is what I have so far.

def Application_Details(request, pk):
    applicants = Applicant.objects.all().filter(application=pk)
    context = {'applicants': applicants}
    return render(request, 'base/application_detail.html', context)

What piece of information are you starting with? Is it the Applicant as shown in your code at the bottom of your post?

Also, I know you’ve done a lot of refactoring of your models. Can you repost your current models? I don’t think we need to see all the fields, just the ForeignKeys, any PrimaryKeys that might be defined, and maybe one or two representative fields for each model. It’s going to be a lot easier to talk about this in the context of your real models than trying to address this in the abstract.

Models

class Person(models.Model):
    first_name = models.CharField(max_length=200)
    middle_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)


class Address(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)


class Employment(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)


class Application(models.Model):
    property = models.ForeignKey(Property, on_delete=models.CASCADE)

class Applicant(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    application = models.ForeignKey(Application, on_delete=models.CASCADE)

    class Meta:
        unique_together = [['person', 'application']]

List View

class ApplicationListView(ListView):
    model = Application
    template = 'base/application_list.html'

    def get_queryset(self):
        return Application.objects.all() 

List view HTML

    <section>
        {% for app in application_list %}
            <div class="container p-2" >
                <a class="btn btn-primary" href="{% url 'application_detail' pk=app.id %}">View application</a>
                ({{ app.id }})
                {{ app.property }}
                {{ app.application_date }}
            </div>
        {% endfor %}
    </section>