User + User Profile update with Class Based View

I extended User (django.contrib.auth.models) with a profile (UserProfile). I have a problem to implement a view where I can edit both, User and UserProfile in one form.
The issue I have is quite simple: only “main” form is saved. Additional one is not saved.
Please help to get it fixed as I’m totally out of options now. I use Django 3.1.7.

Models:

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    picture = models.ImageField(default='users/default_user.png', upload_to='users', blank=True, null=True)

Forms:

class UserUpdateForm(UserChangeForm):
    class Meta:
        model = User
        fields = ['first_name', 'last_name']

class ProfileUpdateForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ['picture']

View:

class UserSettings(UpdateView):
    template_name = 'users/settings.html'
    context_object_name = 'user'
    model = UserProfile
    form_class = ProfileUpdateForm

    def get_success_url(self):
        return reverse('users:user-settings', kwargs={'pk': self.get_object().id})

    def get_context_data(self, **kwargs):
        context = super(UserSettings, self).get_context_data(**kwargs)
        context['user_form'] = UserUpdateForm(instance=self.request.user)
        return context

Template:

                <form method="post">
                        {% csrf_token %}
                    {{ user_form }}
                    {{ form }}
                        <button type="submit" class="btn btn-primary">Submit</button>
                    </form>

The generic class-based views are only designed to work with one form/model.

<opinion> The easiest way to handle this is to not create a second form. Add the two name fields from your UserUpdateForm as form fields in ProfileUpdateForm. Then, override the form’s save method to take those two fields and use them to update the corresponding User model.
<opinion>

I flagged the above as an opinion, because there are other ways to handling it. However, I think that’s the easiest way.

Thats smart approach! Thanks for sharing.

Now I need to assign it somehow. Should I use form_valid? It doesn’t work.

def form_valid(self, form):
    user_form = form.save(commit=False)
    UserProfile.user.last_name = user_form.last_name
    UserProfile.user.first_name = user_form.first_name
    user_form.save()

Ok, I’m confused here by this snippet. What other changes have you made to the code? Did you add the User fields to the Profile form, or did you add the Profile field to the User form?

Also, for clarity, when you call the save method of a model form, the return value is not a form - it’s the model that has been saved. (Or is prepared to be saved with the commit=False). Calling it “user_form” is misleading - if the form is the User form, then what you have at that point is a User object.

Here is the code, sorry for missing it in last post. Please help with this solution, I’m lost.

Forms:

class ProfileUpdateForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ['picture']

class UserProfileUpdateForm(ProfileUpdateForm):
    first_name = forms.CharField(max_length=32)
    last_name = forms.CharField(max_length=32)

    class Meta(ProfileUpdateForm.Meta):
        fields = ProfileUpdateForm.Meta.fields + ['first_name', 'last_name']

Views:

class UserSettings(UpdateView):
    template_name = 'users/settings.html'
    context_object_name = 'user'
    queryset = UserProfile.objects.all()
    form_class = UserProfileUpdateForm

    def get_success_url(self):
        return reverse('users:user-settings', kwargs={'pk': self.get_object().id})

    def get_context_data(self, **kwargs):
        context = super(UserSettings, self).get_context_data(**kwargs)
        context['profile_form'] = UserProfileUpdateForm(instance=self.request.user.userprofile)
        return context

    def form_valid(self, form):
        user_form = form.save(commit=False)
        User.last_name = user_form.last_name
        User.first_name = user_form.first_name
        user_form.save()

Always keep in mind that a ModelForm is just a form, where Django adds some magic to associate that form with a Model. It doesn’t remove any functionality of a form.

class ProfileUpdateForm(forms.ModelForm):
    first_name = forms.CharField(max_length=32)
    last_name = forms.CharField(max_length=32)
    class Meta:
        model = UserProfile
        fields = ['picture']

So then your view will end up looking something like this:

class UserSettings(UpdateView):
    template_name = 'users/settings.html'
    context_object_name = 'user'
    queryset = UserProfile.objects.all()
    form_class = ProfileUpdateForm

    def get_success_url(self):
        return reverse('users:user-settings', kwargs={'pk': self.get_object().id})

    def get_context_data(self, **kwargs):
        context = super(UserSettings, self).get_context_data(**kwargs)
        user = self.request.user
        context['profile_form'] = ProfileUpdateForm(
            instance=self.request.user.userprofile,
            initial={'first_name': user.first_name, 'last_name': user.last_name}
        )
        return context

    def form_valid(self, form):
        profile = form.save()
        user = profile.user
        user.last_name = form.cleaned_data['last_name']
        user.first_name = form.cleaned_data['first_name']
        user.save()

This may not be 100% correct, I’m “live typing” this in the forum, but this is the general direction you’ll want to go.

2 Likes

Yes, this is finally what I was looking for. Thanks for your answer and time spend on this.

Here is final version:
Form:

class ProfileUpdateForm(forms.ModelForm):
    first_name = forms.CharField(max_length=32)
    last_name = forms.CharField(max_length=32)
    class Meta:
        model = UserProfile
        fields = ['picture']

View:

class UserSettings(LoginRequiredMixin, UpdateView):
    template_name = 'users/settings.html'
    context_object_name = 'user'
    queryset = UserProfile.objects.all()
    form_class = ProfileUpdateForm

    def get_context_data(self, **kwargs):
        context = super(UserSettings, self).get_context_data(**kwargs)
        user = self.request.user
        context['profile_form'] = ProfileUpdateForm(instance=self.request.user.userprofile, initial={'first_name': user.first_name, 'last_name': user.last_name})
        return context

    def form_valid(self, form):
        profile = form.save(commit=False)
        user = profile.user
        user.last_name = form.cleaned_data['last_name']
        user.first_name = form.cleaned_data['first_name']
        user.save()
        profile.save()
        return HttpResponseRedirect(reverse('users:user-profile', kwargs={'pk': self.get_object().id}))

Cool! Glad to see you’ve got it working.

But there is no need for you to do the “double save” on the profile model. You’re not modifying the profile model in this method, and the related User model must already exist.

Without this last save it was not working, when I modified the fields which belongs to UserProfile itself (like picture and more I have). Use case was: edit first_name and eg. picture at once.
user.save is saving data for User model and profile.save for UserProfile model.

Did you still have the commit=False? That’s what you don’t need. You can remove that parameter and the second save.