Question

How do I perform a Django form validation for an unknown number of fields specified by the user? In my case, form allows a user to create a music album with any number of tracks and associate it to an artist. The view asks for how many tracks there are and then generates a form with that many input fields.

Form:

class NumberOfTracks(forms.Form):
    track_no = forms.IntegerField()

class CustomAlbumAdmin(forms.Form):
    artist = forms.CharField(max_length=150)
    album = forms.CharField(max_length=150)
    track_no = forms.IntegerField()
    track = forms.CharField(max_length=150)

View:

def album_admin(request):
    if request.GET.get('track_no'):
        number_of_tracks = request.GET.get('track_no')
        artists = Artist.objects.all()
        return render(request, 'customadmin/album_admin1.html', {
            'number_of_tracks': number_of_tracks,
            'tracks': range(1, int(number_of_tracks) + 1),
            'artists': artists,
        })

    elif request.method == 'POST':
        form = CustomAlbumAdmin(request.POST)
        print form
        artist = request.POST['artist']
        album = request.POST['album']
        all_tracks = request.POST.getlist('track')

        create_album = CreateAlbum(artist=artist, album=album, tracks=all_tracks)
        create_album.save_album()
        create_album.save_tracks()

        form = NumberOfTracks()
        return render(request, 'customadmin/no_tracks1.html', {
            'form': form,
        })

    else:
        form = NumberOfTracks()
        return render(request, 'customadmin/no_tracks1.html', {
            'form': form,
        })

(Just so it's clear, I used if form.is_valid() and form.cleaned_data but to get this to work thus far, I've had to bypass that in favor of getting the raw POST data)

Part of what's confusing me is that I've customized my form template to add a number input fields with name="track" depending on user input (EX: create an album with 13 tracks). When I go into my view to print form = CustomAlbumAdmin(request.POST) it gives a very simple table based on my form: one artist, one album, one track, and one track_no so validating against this will of course return False unless I have an album with just one track.

Here's the template:

{% extends 'base.html' %}

{% block content %}

<form action="/customadmin/album1/" method="POST">{% csrf_token %}
    <select name="artist">
    {% for entry in artists %}
        <option value="{{ entry.name }}">{{ entry.name }}</option>
    {% endfor %}
    </select>
    {{ form.non_field_errors }}
    <div class="fieldWrapper">
            {{ form.album.errors }}
            <label for="id_album">Album:</label>
            <input id="id_album" maxlength="150" name="album" type="text" />
    </div>
    <div class="fieldWrapper">
        {{ form.track.errors }}
            <input type="hidden" name="number_of_tracks" value="{{ number_of_tracks }}">
            {% for e in tracks %}
                <label for="id_track">Track No. {{ forloop.counter }}</label>
                <input id="id_track_{{ forloop.counter }}" maxlength="150" name="track" type="text" /></br>
            {% endfor %}
    </div>
    <p><input type="submit" value="Save album" /></p>
</form>

{% endblock %}

The one way I was thinking of approaching this was to create a custom clean_track method that takes a list of all the tracks entered as I've done in the view with all_tracks = request.POST.getlist('track') but not sure how to do that.

A related question I have is if I can customize validation based on POST data. The first way I approached this was to generate incremented inputs with name="track_1", name="track_2", etc.,. and then trying to validate based on that. However, I wouldn't be able to use request.POST.getlist('track') in that case.

Was it helpful?

Solution

It might be a better approach to use formsets instead.

class AlbumForm(forms.Form):
     artist = forms.CharField(max_length=150)
     name = forms.CharField(max_length=150)

class TrackForm(forms.Form):
     track_no = forms.IntegerField()
     name = forms.CharField(max_length=150)


# In view

from django.forms.formsets import formset_factory

TrackFormSet = formset_factory(TrackForm)
if request.method == 'POST':
    track_formset = TrackFormSet(request.POST)
    album_form = AlbumForm(request.POST)
    if track_formset.is_valid() and album_form.is_valid():
        save_album(album_form, track_formset)
else:
    track_formset = TrackFormSet()
    album_form = AlbumForm()

And in save_album you can just iterate through track_formset.ordered_forms to get each form in the formset:

for form in track_formset.ordered_forms:
    data = form.cleaned_data
    # Do what you want with the data

This can be even more powerful if you use model formsets because you can set a foreign key in the track model that points to the album model, and Django can save them automatically for you.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top