0

I am writing a writing a webapp that is basically just a form, and it has a button that duplicates a field so that multiple items can be entered. I can't use a SelectMultiple field or any of its variations because there is not a set number of choices to choose from. The user should be able to enter whatever they want into the fields and they must be saved in the model and linked to a record through a manytomany field. Here is a jsfiddle link for demonstration.

HTML

<form>
    <label>Field 1
        <textarea rows="3"></textarea>
    </label>
    <label>Multiple Values Possible</label>
    <div>
        <input type="text">
        <select>
            <option value="1">1</option>
            <option value="2">2</option>
        </select>
    </div>
    <button id="add_div">Add</button>
</form>

JS

add_div = document.getElementById("add_div");
add_div.onclick = function () {
    var div = this.previousElementSibling;
    var new_div = div.cloneNode(true);

    this.parentNode.insertBefore(new_div, this);

    return false;
}.bind(add_div);

I cannot figure out how to create the form backend for this. There aren't any field classes that can take in a variable amount of data and validate each one against another field.

What I have tried to do is create a MultiWidget/MultiValueField for the textbox/select dropdown pair, and then subclass my MultiValueField in a class closely following django's ModelMultipleChoiceField. I got stuck trying to get the form field to work with templates, allowing me to add all fields back to the rendered page when rendering with a particular form instance (like how when you use the CheckboxSelectMultiple widget, boxes that are checked in a form instance are rendered checked)

Is there any way to do this and have the ModelForm's save method also save the manytomany fields properly? I know I can override the form's save method and do something like in this stackoverflow question, but I would rather have all the save logic handled by the form fields themselves.

Community
  • 1
  • 1
Brian Schlenker
  • 4,966
  • 6
  • 31
  • 44

3 Answers3

0

Based on looking at your example jsfiddle, it looks like you don't really need a "Choice Field", what you're looking for are Formsets.

In essence, you would have 2 forms on the page, one which is a normal form and would take care of Field 1, and one which is a Formset which deals with all the many-to-many relations for Field 2

Field2FormSet = formset_factory(Field2ToForm)

Make sure you output the management_form which you can clone with your "add" button.

Thomas
  • 11,757
  • 4
  • 41
  • 57
0

What you are probably looking for is an inline formset, which can only be used if you are using django models (which you hinted at in your question).

Check out this guide: http://lab305.com/news/2012/jul/19/django-inline-formset-underscore/.

For the lazy, here is a quick example that gets you most of the way there. This app will allow you to continuously add Parent model objects to the database, along with any children that are filled out.

app/models.py

from django.db import models


class ParentModel(models.Model):
    parent_field = models.CharField(choices=[(1, 1), (2, 2)])


class ChildModel(models.Model):
    parent = models.ForeignKey(ParentModel)
    child_field = models.IntegerField(choices=[(1, 1), (2, 2)])

app/views.py

from app import models
from django import forms
from django.forms.models import inlineformset_factory
from django.template import RequestContext, Template
from django.http import HttpResponse


class ParentForm(forms.ModelForm):

    class Meta:
        model = models.ParentModel


ChildFormSet = inlineformset_factory(models.ParentModel, models.ChildModel)


def test_view(request):
    if request.method == "POST":
        form = ParentForm(request.POST, request.FILES)
        formset = ChildFormSet(request.POST, request.FILES, form.instance)
        if form.is_valid() and formset.is_valid():
            form.save()
            formset.save()
        else:
            pass # Handle validation error
    template = Template(
        "<form action='<url for view>' method='post'>"
             "{% csrf_token %}"
             "{{ form.as_p }}"
             "<p>{{ formset.as_table }}</p>"
             "<input type='submit' value='Submit'/>"
        "</form>"
    )
    context = RequestContext(request, {
        "form": ParentForm(),
        "formset": ChildFormSet(),
    })
    return HttpResponse(template.render(context))

What is shown above will only allow you add up to three children (the default number of extra forms the inline form set produces). To add dynamically, you are going to have to add some java script that creates new forms in the form set on the client side. For that, I suggest you look at the guide I posted above since I don't think I can do better job of explaining it.

Kevin S.
  • 628
  • 6
  • 9
0

Thanks to @Kevin and @Thomas for pointing me towards formsets! Here is how I did it:

models.py

from django.db import models

class RelatedField(models.Model):
    field1 = models.CharField(max_length=50)
    field2 = models.IntegerField(choices=[(x, x) for x in xrange(1, 11)])

class Record(models.Model):
    user     = models.ForeignKey(User)
    field    = models.CharField(max_length=20)
    relatedA = models.ManyToManyField(RelatedField, related_name='relatedA')
    relatedB = models.ManyToManyField(RelatedField, related_name='relatedB')

views.py

def getIndexContext(data):
    if data is None:
        recordForm  = RecordForm()
        relatedFormA = RelatedFormSet(queryset=RelatedField.objects.none(), prefix='related-a')
        relatedFormB  = RelatedFormSet(queryset=RelatedField.objects.none(), prefix='related-b')
    else:
        recordForm  = RecordForm(data)
        relatedFormA = RelatedFormSet(data, prefix='related-a')
        relatedFormB  = RelatedFormSet(data, prefix='related-b')

    return {
        'form': recordForm,
        'relatedA': relatedFormA,
        'relatedB': relatedFormB,
        'title': 'Index',
    }   


def index(request):
    if request.method == 'GET':
        return render(request, 'record/index.html', getIndexContext(None))
    else:
        context = getIndexContext(request.POST)
        form = context['form']
        relatedA = context['relatedA']
        relatedB = context['relatedB']
        if form.is_valid() and relatedA.is_valid() and relatedB.is_valid():
            obj = form.save(commit=False)
            obj.user_id = request.user 
            obj.save()
            form.save_m2m()

            instances = relatedA.save()
            obj.relatedA.add(*instances)

            instances = relatedB.save()
            obj.relatedB.add(*instances)

            return HttpResponse('success!')
        else:
            return render(request, 'record/index.html', context)

And then some javascript that can duplicate the fields in the formsets, and increment the names by one.

Brian Schlenker
  • 4,966
  • 6
  • 31
  • 44