Combining different forms in Django, the proper way.

Note: All the code used here is available in a github repo HERE.

Django bills itself as a framework for “Web Developers with deadlines”. This is absolutely true. With the ease of python and Django’s “batteries included” philosophy, you can get an incredible amount of stuff done in a short amount of time and have your project production ready in no time.

The best example of this is the many myriad ways Django makes things easier for you, especially in form processing. Django forms are disparate from everything enough that you can heavily customize how you use them but are functional enough that you still don’t need a lot of the boilerplate code no matter the customization.

To illustrate this, let’s take the following problem. We have to build a registration system for an e-sports event. For this event, there’s two kinds of people attending: Players and Spectators. Both players and spectators have a basic set of information required of them, but they also have some details that are specific to them. For example, spectators might have a coupon code or a ticket type while players will have a team name and a game they will have to select.

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

from django.db.models.signals import post_save
from django.dispatch import receiver


class Player(models.Model):
    user = models.ForeignKey(User)
    GAME_CHOICES = (
                   ('CS', 'Counter-Strike'),
                   ('DOTA', 'DotA'),
                   ('LOL', 'League of Legends'))

    game = models.CharField(max_length=4, choices=GAME_CHOICES)
    team_name = models.CharField(max_length=100,
                                 help_text='Enter the name of your team')


class Spectator(models.Model):
    user = models.ForeignKey(User)
    TICKET_CHOICES = (
        ('S', 'Silver'),
        ('G', 'Gold'),
        ('P', 'Platinum'))

    ticket_class = models.CharField(max_length=2, choices=TICKET_CHOICES)

    coupon_code = models.CharField(max_length=5)


class BasicInfo(models.Model):
    user = models.OneToOneField(User)
    name = models.CharField(max_length=100)
    user_type = models.CharField(max_length=20)


@receiver(post_save, sender=User)
def create_basic_info(sender, instance, created, **kwargs):
    if created:
        BasicInfo.objects.create(user=instance)


@receiver(post_save, sender=User)
def save_basic_info(sender, instance, **kwargs):
    instance.basicinfo.save()

This is the models.py for our program. BasicInfo houses the basic user  information by extending the User model, and adds the user_type field to all users. The Player and the Spectator models contain the specific fields for each individual type of user.

Segregating these user types is helpful because it allows us to do interesting things in the future. We can customize entire views around these two different kinds of users by simply checking that the user, or we can have other models use only this specific type of user, and so on. For example if we had a Ticket model, it could have a ForeignKey field to users of type Spectator, thus excluding Players entirely. But we won’t do that here.

Moving on, we will now create a template. Our template will be simple (note that the Bootstrap stuff is omitted, and it includes a JS file which will be described later).

{% csrf_token %}
{{ basic_info_form.as_table }}

{% if second_form %}
{{ second_form.as_table }}
{% endif %}

Django forms include three functions that one can use to render them, .as_ul(), as_table() and as_p()` I have used as_table() (in the view code) here for formatting, but one can use whatever suits them.

Include the form class in the ​{{ }} template tokens automatically renders the proper html code for the form. However, and this is where Django’s flexibility shines, it doesn’t include the

tag or the submit button (). This may seem tedious for developers, but this allows us to properly format those and put them as we see fit and is the reason we can combine two separate forms in the first place.

Finally we have the forms.py and the views.py. The forms.py simply contains forms for each model and a save() for simplicity.

class BasicInfoForm(UserCreationForm):
    choices = [(0, '--------'), (1, 'Spectator'), (2, 'Player')]
    user_type = forms.ChoiceField(choices=choices, initial=None)

    class Meta:
        model = User
        fields = ['username', 'password1', 'password2', 'email', 'user_type']

    def save(self, commit=True):
        user = super(UserCreationForm, self).save(commit=False)

        LOG.debug("user_type from form:" + self.cleaned_data['user_type'])
        if commit:
            user.save()  # First save the created user,
            user.basicinfo.user_type = self.cleaned_data['user_type']
            user.save()  # save the basic info

        return user


class PlayerForm(forms.Form):
    game = forms.ChoiceField(models.Player.GAME_CHOICES)
    team_name = forms.CharField(max_length=100)

    def save(self, user, commit=True):
        player = models.Player.objects.create(user=user)
        player.user = user
        player.game = self.cleaned_data['game']
        player.team_name = self.cleaned_data['team_name']

        if commit:
            player.save()

        return player


class SpectatorForm(forms.Form):
    ticket_class = forms.ChoiceField(models.Spectator.TICKET_CHOICES)
    coupon_code = forms.CharField(max_length=5)

    def save(self, user, commit=True):
        spectator = models.Spectator.objects.create(user=user)
        spectator.ticket_class = self.cleaned_data['ticket_class']
        spectator.coupon_code = self.cleaned_data['coupon_code']

        if commit:
            spectator.save()

        return spectator

The beauty of Django is that it renders the proper tag code in the template simply because we’ve used a ChoiceField() in the form. Finally we have our register view.




def register(request):

    if request.method == 'POST':
        basic_info_form = BasicInfoForm(request.POST)
        option = request.POST.get('user_type')
        if option is not None:
            if option == '1':
                LOG.debug("user_type 1")
                second_form = SpectatorForm(request.POST)

            if option == '2':
                LOG.debug("user_type 2")
                second_form = PlayerForm(request.POST)

        if basic_info_form.is_valid():
            LOG.debug("BASIC INFO FORM VALID")
            user = basic_info_form.save()
            LOG.debug("new user pk = " + str(user.pk))
            userid = user.pk

            if second_form.is_valid():
                second_form.save(user=user)

                return HttpResponse("

SUCCESS!!

“) else: return render(request, ‘new.html’, {‘basic_info_form’: basic_info_form.as_table(), ‘second_form’: second_form.as_table()}) else: basic_info_form = BasicInfoForm() option = request.GET.get(‘option’) if option is not None: if option == ‘Player’: second_form = PlayerForm() if option == ‘Spectator’: second_form = SpectatorForm() return HttpResponse(second_form.as_table()) return render(request, ‘new.html’, {‘basic_info_form’: basic_info_form.as_table() })

This view is called several times in various scenarios. The first time is a GET request when the user navigates to the page. When this happens, the page simply returns the template with the BasicInfo form. When the user selects a user_type, a new GET request is generated with the following script


$(document).ready(function() {
    $("#id_user_type").change(function () {
        $.ajax({
            url: '/register/new',
            data: {option: $("#id_user_type option:selected").text()},
            success: function (html) {
                $("#form2").html(html);
            },
            method: 'GET'
        });
    })
})

The option is sent to the view and an HTTP response with either the Player form or the Spectator form is returned depending on which option is selected.

The third time this view is called is when the user submits the form. The proper form is chosen depending on the user’s selection of the type, and the data is saved using the forms’ save() method.

It should be noted that another way to do this, is to have a separate view to return each type of form the user is given. However, for our purposes, a simple, but crude, if-else approach works fine.

Once the user properly submits the data and validation is completed, the user is simply given a page with the words “SUCCESS!!” printed on it. Generally the user would be directed to the proper page depending on the specific project.

So this is how Django’s immense flexibility can help a developer eliminate a large amount of tedious boilerplate while still offering enormous customization.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s