Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django inline form with custom forms

Hi I have a domain model, used in Django app, which I'd like to present on a single form. I've created my application with custom ModelForms (not much changes, some fields excluded etc). The model's dependencies are as follows:

Complaint
   \
    .--- CarInfo
    .--- Customer

My view function looks like this:

def make(request):
  if request.method == 'POST':
    parameters = copy.copy(request.POST)
    complaint = Complaint()
    carInfo = CarInfo()
    customer = Customer()

    customer_form = CustomerForm(parameters, instance=customer)
    carInfo_form = CarInfoForm(parameters, instance=carInfo)
    parameters['complaint_date'] = get_current_date()
    parameters['customer'] = 1 # dummy value to allow validation success
    parameters['car_info'] = 1 # dummy value to allow validation success
    form = ComplaintForm(parameters, instance=complaint)
    if form.is_valid() and customer_form.is_valid() and carInfo_form.is_valid():
      carInfo_form.save()
      customer_form.save()
      parameters['customer'] = customer.id
      parameters['car_info'] = carInfo.id
      form = ComplaintForm(parameters, instance=complaint)
      form.save()
      return index(request)
  else:
    form = ComplaintForm()
    carInfo_form = CarInfoForm()
    customer_form = CustomerForm()
  return render_to_response('complaints/make_complaint.html', {'complaint_form' : form, 'customer_form' : customer_form, 'carInfo' : carInfo_form})

I don't like this approach too much, moreover it doesn't work in all environments -thou I haven't found the reason for it not working. I've been looking into fixing this code a bit and found something like inline formset (http://docs.djangoproject.com/en/dev/topics/forms/modelforms/#inline-formsets). This solution seems ok, but since my forms are custom tailored I can use it.

Perhaps someone could offer me some advice on how to properly solve such case. Cleaner solutions are much appreciated.

EDITED There is a case for me, where this solutions just doesn't work. Despite of setting dummy values on foreign keys, when I call is_valid() I get FALSE, with error message saying that these fields are not set. I'm observing this problem with django 1.2.5 - it occurs on server I intent to run this app, however my laptop (also django 1.2.5) doesn't have this problem.

like image 799
Marcin Cylke Avatar asked Mar 02 '11 17:03

Marcin Cylke


3 Answers

You can change your Complaint model's complaint_date to something like this

complaint_date = models.DateField(default=datetime.date.today())

that way you can get rid of

parameters['complaint_date'] = get_current_date()

As for your multiform view you can use an unbound forms for you desired behavior.

By excluding the fk to the car and customer on the complaint form, the forms should validate. Check the .is_valid() of all 3 forms at the same time and then save the 2 forms that your complaint object is dependent on, create the complaint object with out commit to database (commit=False), add the id's of the customer and car to that object, then save.

in your view.

def make(request):
    if request.method == 'POST':
        customer_form = CustomerForm(request.POST)
        carInfo_form = CarInfoForm(request.POST)
        form = ComplaintForm(request.POST)

        if form.is_valid() and customer_form.is_valid() and carInfo_form.is_valid():
            car_instance = carInfo_form.save()
            customer_instance = customer_form.save()

            complaint_instance = form.save(commit=False)
            complaint_instance.car_info = car_instance
            complaint_instance.customer_info = customer_instance          
            complaint_instance.save()

            return index(request)
    else:
        form = ComplaintForm()
        carInfo_form = CarInfoForm()
        customer_form = CustomerForm()

    context = { 'complaint_form' : form,
                'customer_form' : customer_form, 
                'carInfo' : carInfo_form,
              }
    return render_to_response('complaints/make_complaint.html', context, context_instance=RequestContext(request))

edit:

models look like this:

class CarInfo(models.Model):
    some_car_info = models.CharField()

class Customer(models.Model):
    some_customer_info = models.CharField()

class Complaint(models.Model):
    car_info = models.ForeignKey(CarInfo)
    customer_info = models.ForeignKey(Customer)
    some_complaint_info = models.CharField()

forms.py should look like this:

class CarInfoForm(forms.ModelForm):
    class Meta:
        model = CarInfo

class CustomerForm(forms.ModelForm):
    class Meta:
        model = Customer

class ComplaintForm(forms.ModelForm):
    class Meta:
        model = Complaint
        exclude = ('car_info', 'customer_info',) # or include = ('some_complaint_info',)

Lets walk through the view I wrote out above: form in view docs

  • On first pass, there is no request.method so we create 3 unbound forms.

    else:
        form = ComplaintForm()
        carInfo_form = CarInfoForm()
        customer_form = CustomerForm()
    
  • these forms are passed to the template and rendered.

  • When the view is called again with request.method == "POST" evaluating true, we create the 3 bound form instances using the data from our request.POST.

    if request.method == 'POST':
        customer_form = CustomerForm(request.POST)
        carInfo_form = CarInfoForm(request.POST)
        form = ComplaintForm(request.POST)
    
  • Next we call the .is_valid() method on each form. In our example because we excluded the 'customer_info' and 'car_info' foreign key fields in our complaint modelform, each form is only checking to see that the char input field validates.

  • If validation all passes then we can start saving our forms to models and this where we need to be careful about populating our complaint's required fk's:

        if form.is_valid() and customer_form.is_valid() and carInfo_form.is_valid():
            car_instance = carInfo_form.save()
            customer_instance = customer_form.save()
    
  • With those 2 forms we can call the .save() as usual. We will however assign the return value to car_instance and customer_instance. These will contain the instances of the CarInfo and Customer models we just created using the .save() method on the form.

  • Next, using the commit=False argument in the .save() method, we are able to create an object from the bound form (containing the request.POST data) and not save it to the database.

            complaint_instance = form.save(commit=False)
            complaint_instance.car_info = car_instance
            complaint_instance.customer_info = customer_instance          
            complaint_instance.save()
    
  • To make this clearer, you could also have created a new Complaint object like this:

    complaint_info = form.cleaned_data.get('some_complaint_info')
    complaint_instance = Complaint(car_info=car_instance, customer_info=customer_instance, some_complaint_info=some_complaint_info)
    complaint_instance.save()
    
  • render

like image 81
dting Avatar answered Oct 09 '22 14:10

dting


I think you already have most clean and simple approach, but if you want to go with formsets try these links:

  • http://www.slideshare.net/kingkilr/forms-getting-your-moneys-worth
  • Django multiple form factory

EDIT. I guess you can experiece problems because of that dummy values (and modifiying request.POST, I can continue guessing :), but @kriegar showed how you can avoid this. Anyway there is nothing hard in saving several forms in one view; Django Forms support such case well enough. My point is that doing this explicitly is the most cleanest and simplest way, formsets will not improve situation much.

like image 25
Vladimir Mihailenco Avatar answered Oct 09 '22 13:10

Vladimir Mihailenco


Probably formset factory and inline formset should solve your problem... You can modify or override form fields created from models, For sub models, you can use inline formsets...

Formsets and inline formsets...

A possible solution:

in your form definition:

class CarInfoFrm(forms.ModelForm):
    class Meta:
        model = CarInfo
        fields = (....)
carInfoForm = inlineformset_factory(Complaint, CarInfo, form=carInfoFrm,)
CustomerForm = inlineformset_factory(Complaint, Customer, form=carInfoFrm,)

In your view:

complaint = Complaint()
carInfo = CarInfo()
customer = Customer()

cus_form = CustomerForm(parameters, instance=complaint)
car_form = CarInfoForm(parameters, instance=complaint)
comp_form = ComplaintForm(parameters, instance=complaint)

if cus_form.is_valid() and ...... :
    comp = comp_form.save(commit=False)#do not save it yet
    comp.<attr> = "some_value" #you can edit your data before save...
    comp.save()
    car = car_form(commit=False)
    # do as complaint form... edit and save... 

EDIT: I make a misteke when defining instance parameter while saving inlineforms. So i correct it...

When you update existing records, you will not have problem, but it is better to use it like:

if comp_form.is_valid():
    comp = comp_form.save(commit=False)
    comp.<attr> = "some_value"
    comp.save()
if car_form.is_valid():
    # edit if neccessary then save...

if cust_form.is_Valid():
    # edit if neccessary then save...

What makes easier is, when you define your form, you set a parent form via a foreign key

carInfoForm = inlineformset_factory(Complaint, CarInfo, form=carInfoFrm,)

And when you update using inline form, you initialize it with your parent Compalaint record,

car_form = CarInfoForm(parameters, instance=complaint)

So, car_form do not accept an carInfo instance, but a complaint instance(that was my mistake in my first answer, so i correct it). if it creates a new record, it automatically binds it to related complaint record. If it is an update, it only updated fields that you want.

For me iti is better to use moehods of the framework instead of writing your owns. By doing this, you will guarantee to have all your validation checks made by django.

like image 20
FallenAngel Avatar answered Oct 09 '22 14:10

FallenAngel