1
\$\begingroup\$

I'm learning Django and making my (technically second) project.

The project features several apps, including player_interface and manager_interface.

Here's the player_interface/models.py:

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


class PlayerProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.PROTECT,
                                limit_choices_to={'is_staff': False})
    balance = models.PositiveIntegerField(default=0)

    def __str__(self):
        return f'PlayerProfile user_id={self.user_id} balance={self.balance}'

In the manager_interface app, I want to create users together with corresponding player profiles. So I made a form (in manager_interface/forms.py):

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

from player_interface.models import PlayerProfile


PlayerProfileUserFormset = forms.inlineformset_factory(
    User, PlayerProfile, fields=('balance',),
    extra=1, max_num=1, can_delete=False)


class PlayerUserForm(forms.ModelForm):
    def __init__(self, data=None, instance=None, **kwargs):
        super().__init__(data=data, instance=instance, **kwargs)
        self.formset = PlayerProfileUserFormset(data=data,
                                                instance=self.instance)

    def is_valid(self):
        return super().is_valid and self.formset.is_valid()

    @transaction.atomic
    def save(self):
        super().save()
        self.formset.save()
        return self.instance

    class Meta:
        model = User
        fields = ('username', 'password')

And this is how I use this form in manager_interface/views.py:

<...>

class CUPlayerMixin:
    template_name = 'manager_interface/players_form.html'
    form_class = PlayerUserForm
    success_url = reverse_lazy('players_list')


class CreatePlayerView(CUPlayerMixin, edit_views.CreateView):
    pass


class UpdatePlayerView(CUPlayerMixin, edit_views.UpdateView):
    queryset = User.objects.filter(is_staff=False)
    slug_field = 'username'
    slug_url_kwarg = slug_field

And the relevant template bit is:

{% block content %}
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <div>{{ form.formset.as_p }}</div>
        <button type="submit">Submit</button>
    </form>
{% endblock %}

The code above works.

My question is: is this a good, Django way to handle a form for both a model and its association?

In this case, I edit both the User model with the associated PlayerProfile. That's why I decided to use the formset: it would automatically populate the subform for the player profile and then save it with the self.formset.save() part, keeping track of the association and all.

Thank you very much!

\$\endgroup\$
  • \$\begingroup\$ The way I understand it, I can call formset.save() and it will automatically create the player profile with the user instance set, etc. If I understand this correctly, if I just make a simple form, I'll have to create the profile instance manually. \$\endgroup\$ – art-solopov Feb 6 at 9:01
  • \$\begingroup\$ Yes (PlayerUserForm is, in fact, a ModelForm), but I would still have to create the PlayerProfile instance manually, would I not? \$\endgroup\$ – art-solopov Feb 6 at 9:15
  • \$\begingroup\$ I just use {{ form.as_p }} and then {{ form.formset.as_p }}. \$\endgroup\$ – art-solopov Feb 6 at 9:30
2
\$\begingroup\$

You have the right approach of creating two separate forms for your two models involved (User and PlayerProfile) but I think the formset is overkill and not the right tool for the job anyway.

I’d rather create the ModelForms more naturally:

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('username', 'password')


class PlayerProfileForm(forms.ModelForm):
    class Meta:
        model = PlayerProfile
        fields = ('balance',)

And glue them together with an ad-hoc "form" along the lines of:

class PlayerUserForm:
    def __init__(self, *args, **kwargs):
        self.user_form = UserForm(*args, **kwargs)
        self.player_form = PlayerProfileForm(*args, **kwargs)

    def is_valid(self):
        # Make sure to **not** short-circuit so as
        # to populate both forms errors, if any. 
        user_valid = self.user_form.is_valid()
        player_valid = self.player_form.is_valid()
        return user_valid and player_valid

    @transaction.atomic
    def save(self):
        user = self.user_form.save()
        player = self.player_form.save(commit=False)
        player.user = user
        player.save()
        return player

And you can add template helpers for good measure:

    def __str__(self):
        return self.as_table()

    def as_table(self):
        return mark_safe(f'{self.user_form.as_table()}\n{self.player_form.as_table()}')

    def as_ul(self):
        return mark_safe(f'{self.user_form.as_ul()}\n{self.player_form.as_ul()}')

    def as_p(self):
        return mark_safe(f'{self.user_form.as_p()}\n{self.player_form.as_p()}')

This is untested but should work as a drop-in replacement for your PlayerUserForm.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.