views:

344

answers:

3

Let's say I'm using the default auth.models.User plus my custom Profile and Address models which look like this:

class Profile(models.Model):
    user = models.OneToOneField(User)
    primary_phone = models.CharField(max_length=20)
    address = models.ForeignKey(Address)

class Address(models.Model):
    country = CountryField(default='CA')
    province = CAProvinceField(default='BC')
    city = models.CharField(max_length=80)
    postal_code = models.CharField(max_length=6)
    street1 = models.CharField(max_length=80)
    street2 = models.CharField(max_length=80, blank=True, null=True)
    street3 = models.CharField(max_length=80, blank=True, null=True)

Now I want to create a registration form. I could create a ModelForm based on User but that won't include fields for the Profile and Address (which are required). So what's the best way to go about building this form? Should I even use ModelForm at all?

Furthermore, how would I use the same form for editing the complex object? I could easily pass an instance of Profile back to it, which holds references to the necessary Address and Profile objects, but how do I get it to fill in the fields for me?

+1  A: 

You should look into the officially recommended way to extend the User model first, as seen in the docs, which I believe comes directly from the project manager's personal blog about the subject. (The actual blog article is rather old, now)

As for your actual issue with forms, have a look at the project manager's own reusable django-profiles app and see if perusing the code solves your issue. Specifically these functions and the views in which they are utilized.

Edited to Add:

I've looked into it a bit (as I needed to do so myself). It seems something like so would be sufficient:

# apps.profiles.models

from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    ...
    birth_date = models.DateField(blank=True, null=True)
    joined = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = 'user profile'
        verbose_name_plural = 'user profiles'
        db_table = 'user_profiles'

class Address(models.Model):
    user = models.ForeignKey(UserProfile)
    ...

# apps.profiles.forms

from django import forms
from django.forms import ModelForm
from django.forms.models import inlineformset_factory
from django.contrib.auth.models import User
from apps.profiles.models import UserProfile, Address

class UserForm(ModelForm):
    class Meta:
        model = User
        ...

class UserProfileForm(ModelForm):
    class Meta:
        model = UserProfile
        ...

AddressFormSet = inlineformset_factory(UserProfile, Address)

I was using "..." to snip content in the code above. I have not yet tested this out but from looking through examples and the documentation on forms I believe this to be correct.

Note I put the FK from the Address model to the UserProfile and not the other way around, as in your question. I believe the inline formsets need this to work correctly.

Then of course in your views and templates you will end up treating UserForm, UserProfileForm, and AddressFormSet separately but they can all be inserted into the same form.

jonwd7
I already am using the officially recommended way of extending the User model. This django-profiles app doesn't appear to be useful either. It's for creating a profile after the user object already exists, not for creating them simultaneously with *one* form.
Mark
**Edit:** Okay, I'm not using the the 'recommended way' verbatim. I'm using OneToOne rather than ForeignKey because that makes more sense to me, but that's the only diff.
Mark
I believe using `ForeignKey` with `unique=True` is recommended for several reasons, one of which is that it would be only one query whereas OneToOneField would cause it to be two. I can't say for sure if the reasons I've read still apply to 1.1 or 1.2.
jonwd7
Having the FK on Address completely defeats the purpose of the address object. Other things have Addresses too. Regardless, you could swap the order around in the `inlineformset_factory` but this still doesn't solve the problem, because I have multiple FKs, not just one. And I still have no idea how that formset is supposed to render.
Mark
Oh, and furthermore, it sounds like with formsets you're allowed to leave the data in that particular form completely blank, which is not what I want (it's required).
Mark
Formsets still have an `is_valid()` method so I have no idea what you're talking about. I did not intend to provide a complete example filled with form validation examples as well.
jonwd7
Yes...they have `is_valid()` but from my understanding, if *all* the form fields are left blank, it won't save that model, because formsets are made for entering multiple objects, not for mixing-and-matching models.
Mark
+1  A: 

I think your are looking for inline formsets with model forms. This helps you to deal with multiple forms on one page and also takes care of foreign key relations.

Update:

Maybe this question helps you too: http://stackoverflow.com/questions/569468/django-multiple-models-in-one-template-using-forms

Felix Kling
This looks like it might have potential, but how do I do an inline formet swith *two* or more foreignkeys?
Mark
If you're talking about two or more foreign keys to *different models* it doesn't matter because you need only specify the model. If it is two or more foreign keys to the same model you use `fk_name` to specify which field. Anyway, I have updated my own answer with an example (yesterday), and you should check it out.
jonwd7
Huh? `inlineformset_factory(Author, Book)`. `Book` has a FK ao `Author`. But I don't know what args that function takes, can I specify more FKs in there? Like `inlineformset_factory(User, Address, Profile)` where Profile has FKs to both User and Address? And then I can render this formset and it will show the forms for all of those in one, and save them all into Profile?
Mark
"Huh?" is right.. I have no idea what you are talking about. You start a `<form>` in your template, then you enter (for example) `user_form`, `profile_form`, and any inline formsets you may have, and then you put in an `</form>' tag!
jonwd7
The point was to combine users, addresses, and profiles all into one form. If I'm going to do them separately, then I'm not seeing what advantage these 'inline formsets' give me.
Mark
+1  A: 

What about using 3 separate ModelForm. One for Address, one for User, and one for Profile but with :

class ProfileForm(ModelForm):
  class Meta:
    model = Profile
    exclude = ('user', 'address',)

Then, process these 3 forms separately in your views. Specifically, for the ProfileForm use save with commit=False to update user and address field on the instance :

# ...
profile_form = ProfileForm(request.POST)
if profile_form.is_valid():
  profile = profile_form.save(commit=False)
  # `user` and `address` have been created previously
  # by saving the other forms
  profile.user = user
  profile.address = address

Don't hesitate to use transactions here to be sure rows get inserted only when the 3 forms are valid.

Clément
Looks like this is the way to go. I'm not happy about it, but it's the only thing that works. I would have liked some "DeepModelForm" where ForeignKeys/OneToOnes get converted to their own form, rather than a `<select>` with existing objects.
Mark
@Clément the new correct way of doing this (1.2 or dev 1.2) is in `clean()` instead of overriding `save()`.
orokusaki
@orokusaki: I don't think he was suggesting I override the save method, just to call it with `commit=False`. @clement: The way I did it was `if a.is_valid() and b.is_vald() and c...`, so I'm hoping there will never be a case where they *aren't* all valid before I start saving them.
Mark