8

I have two models:

class Studio(models.Model):
    name = models.CharField("Studio", max_length=30, unique=True)

class Film(models.Model):
    studio = models.ForeignKey(Studio, verbose_name="Studio")
    name = models.CharField("Film Name", max_length=30, unique=True)

I have a Film form that allows the user to either select a preexisting Studio, or type in a new one:

class FilmForm(forms.Form):
    studio = forms.ModelChoiceField(Studio.objects, required=False)
    new_studio = forms.CharField(max_length=30, required=False, label = "New Studio Name")
    name = forms.CharField(max_length=30, label = "Film Name")

There's validation to assure that the new_studio name doesn't already exist. If the user enters a new_studio, I want to save the studio and then save the new Film.

form = FilmForm(request.POST) 
if form.is_valid(): # All validation rules pass
    std = Studio(name = form.cleaned_data['new_studio'])
    std.save()

But then how do I save the Film instance subject to the brand new studio id? I've seen this question, but what if I have many more fields in the Film model and Film Form? If I use the linked answer, I would have to enter each field:

studio = Studio.objects.get(name=request.POST['new_studio'])
newFilm=Film(name=form.name, studio=studio, field_one = form.field_one, field_two = form.field_two, etc.)

What is the correct way to implement this?

Community
  • 1
  • 1
Ed.
  • 4,439
  • 10
  • 60
  • 78

2 Answers2

6

Really, your only problem is that you've used a standard Form instead of a ModelForm. Form doesn't have a save method because it's not inherently tied to anything (i.e. it doesn't know what to save or where to save to).

However, if you use a ModelForm you need to take care of all logic involved in creating a new studio in the form. This is actually better, though, because then you can just use the form and not worry about anything else: the form holds all the logic needed to properly save itself.

class FilmForm(forms.ModelForm):
    class Meta:
        model = Film

    # only need to define `new_studio`, other fields come automatically from model
    new_studio = forms.CharField(max_length=30, required=False, label = "New Studio Name")

    def __init__(self, *args, **kwargs):
        super(FilmForm, self).__init__(*args, **kwargs)
        # make `studio` not required, we'll check for one of `studio` or `new_studio` in the `clean` method
        self.fields['studio'].required = False

    def clean(self):
        studio = self.cleaned_data.get('studio')
        new_studio = self.cleaned_data.get('new_studio')
        if not studio and not new_studio:
            # neither was specified so raise an error to user
            raise forms.ValidationError('Must specify either Studio or New Studio!')
        elif not studio:
            # get/create `Studio` from `new_studio` and use it for `studio` field
            studio, created = Studio.objects.get_or_create(name=new_studio)
            self.cleaned_data['studio'] = studio

        return super(FilmForm, self).clean()

Then, in your view, all you need is:

if form.is_valid():
    form.save()
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • oh, hmm. Nice! I thought I had to go the long way around to compensate for the possibility of adding a new studio. Thanks! – Ed. Jan 25 '12 at 01:41
  • I'm having one issue though. I created a validation check for a duplicate studio (if a user enters a new_studio that already exists). The code correctly reaches the ValidationError, but seems to continue to clean anyway and gives me the error: Cannot assign None: "Film.studio" does not allow null values. – Ed. Jan 25 '12 at 19:35
  • The `clean` method takes care of all that. If the `new_studio` exists, it will simply get the exisiting studio instead of actually creating a new one (`get_or_create`), so the validation check you have is unnecessary. The only case not covered by the `clean` method above is if `studio` is entered but `new_studio` is not. However, that wouldn't be an error and nothing special needs to be done in that scenario. I imagine whatever validation check you're doing is messing with the values somehow. – Chris Pratt Jan 25 '12 at 21:16
  • gotcha. but when leaving both studio and new_studio blank and after removing that duplicate-check, I'm still getting the error. Revised question here if you care to comment? http://stackoverflow.com/questions/9010852/catching-validation-errors-in-django-forms – Ed. Jan 25 '12 at 21:52
2

I stumbled across this question and I think I got a better answer for future seekers.

Django comes with something called Inline Formsets which helps you manage relationships in a form.

You just have to create your model like you did with their respective ForeignKeys. Then in the Forms.py import this

from django.forms.models import inlineformset_factory

Then:

YOUR_FORMSET = inlineformset_factory(Model1,Model2)

Back on yout views.py you should import both your Form and your new created YOUR_FORMSET.

After doing this, your method should use the inlineformset like this:

def add_question(request):

    if request.method == 'POST':


      form =AddQuestionForm(request.POST, request.FILES)
      if form.is_valid():
          new_question = form.save(commit=False)

          question_formset = QuestionFormset(request.POST, instance=new_question)
          if question_formset.is_valid():
              form.save()
              question_formset.save()
              return HttpResponseRedirect(reverse('polls:detail',args=(new_question.pk,)))
      else:
          print(form.errors)
    else:
        form = AddQuestionForm()
        question_formset = QuestionFormset(instance=Question())
    return render(request, 'polls/add.html', {'form':form, 'question_formset':question_formset,})

I'm using my own example but the idea is explained.

If you want to read a more specific explication read this awesome blog post that I found.

villancikos
  • 519
  • 1
  • 6
  • 13