16

I have a form that lets me first select a product type and then select the product. As i have 1000+ products i use the following to filter the product list to improve performance.

I have the following inlineform in my views.py

OrderLineFormSet = inlineformset_factory(OrderHeader, OrderLine, OrderLineForm, extra = 1)

In my forms.py i check if there is already a product selected. If there is a product selected i only show the products with the same product type to improve load performance. If a product is empty it will load all product options so it will let me save the form after selection.

class OrderLineForm(forms.ModelForm):

def __init__(self, *args, **kwargs):
    super(OrderLineForm, self).__init__(*args, **kwargs)
    self.helper = FormHelper(self)
    self.helper.form_show_errors = True
    self.helper.error_text_inline = False
    if self.instance.product is not None:        
        self.fields['product'] = forms.ModelChoiceField(queryset=Product.objects.filter(product_type_id=self.instance.product_type_id), required=False)

This results in the following form

enter image description here

However, when i change the Product type on an existing form (and then use jQuery to update the Product dropdown) i get an error saving. I know this is because the selection is not an option in the dropdown.

enter image description here

My question: How can i disable this error so it saves the option i selected, regardless of the original options.

Below you will find my views.py for this form

def orderline_formset(request, id=None):

OrderLineFormSet = inlineformset_factory(OrderHeader, OrderLine, OrderLineForm, extra = 1)

orderheader = None
orderid = None
orderheaderid = 0

if id:
    orderid = OrderHeader.objects.get(pk=id)

if request.POST:
    if orderid:
        form = OrderHeaderForm(request.POST, instance=orderid)
        formset = OrderLineFormSet(request.POST,instance=orderid)
    else:
        form = OrderHeaderForm(request.POST)
        formset = OrderLineFormSet(request.POST)

    if form.is_valid() and formset.is_valid():
        if orderid:
            form.save()  # update object
        else:
            orderid = form.save()  # create object
        formset.instance = orderid
        formset.save()
        messages.success(request, 'Order saved succesfully!')
        return HttpResponseRedirect('/orderline_formset/' + str(orderid.pk))

    else:  # form invalid
        messages.error(request, 'Order save error, please check mandatory fields')


else:  # request.GET
    if orderid:
        invoiceheader = "" 
        if orderid.orderheader_invoice:
            invoiceheader = " -- Invoice " + str(orderid.orderheader_invoice) 
        orderheader = "Order " + str(orderid.pk) + invoiceheader

        orderheaderid = orderid.pk
        form = OrderHeaderForm(instance=orderid)
        formset = OrderLineFormSet(instance=orderid)
    else:
        orderheader = "New Order"
        orderheaderid = 0
        form = OrderHeaderForm(instance=OrderHeader())
        formset = OrderLineFormSet(instance=OrderHeader())

return render_to_response("order-add.html", {'form' : form,'formset': formset, 
                            'orderheader': orderheader,
                            'orderheaderid': orderheaderid},
                            context_instance=RequestContext(request))
phicon
  • 3,549
  • 5
  • 32
  • 62
  • If I got it right, you have some options in Product form. When you change an existing order by selecting a new Product Type, it loads new Products that are not presented in the old Product form. So it throws a ValidationError. In this case you need to manually handle this situation and reasign Product form options. – sobolevn May 18 '15 at 16:46
  • when you say you use JQuery to update the product drop down - what do you actually do? Do you run an ajax get to get new products to populate the select? – professorDante May 18 '15 at 17:07
  • 1
    Yes - on change of the product type dropdown – phicon May 18 '15 at 21:35
  • If you use jQuery to send an ajax request to update the Product Type, there must be some function that handles this request. I think you can change the validation option in that function. – ljk321 May 19 '15 at 03:24

4 Answers4

11

Override ModelChoiceField, for example:

class MyModelChoiceField(ModelChoiceField):

   def to_python(self, value):
        try:
            value = super(MyModelChoiceField, self).to_python(value)
        except self.queryset.model.DoesNotExist:
            key = self.to_field_name or 'pk'
            value = Product.objects.filter(**{key: value})
            if not value.exists():
               raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
            else:
               value= value.first()
       return value

And use it in your form.

self.fields['product'] = MyModelChoiceField(queryset=Product.objects.filter(product_type_id=self.instance.product_type_id), required=False)
ruddra
  • 50,746
  • 7
  • 78
  • 101
  • 3
    This worked, great answer. I had to remove try: value =super().to_python(value) as this raised and error and it works fine without it, and thus changing "except" to "if". – phicon May 24 '15 at 12:19
2

Updating @ruddra 's answer for Django 1.11:

    class DynamicModelChoiceField(ModelChoiceField):
    def to_python(self, value):
        try:
            value = super().to_python(value)
        except ValidationError:
            key = self.to_field_name or 'pk'
            value = self.queryset.model.objects.filter(**{key: value})
            if not value.exists():
                raise
            value = value.first()
        return value
GluePear
  • 7,244
  • 20
  • 67
  • 120
Matthew
  • 559
  • 7
  • 10
1

You must change

    self.fields['product'] = forms.ModelChoiceField(queryset=Product.objects.filter(product_type_id=self.instance.product_type_id), required=False)

To

    self.fields['product'] = forms.ModelChoiceField(queryset=Product.objects.all(), required=False)

But you already know that and don't want it for performance reasons so your other solution is changing it to

self.fields['product'] = ModelChoiceField(queryset=Product.objects.all(),  widget=forms.HiddenInput, required=False)

then inside your template you manually construct your <select> tag and using JS handle onchange event and make it update the product field

Ramast
  • 7,157
  • 3
  • 32
  • 32
0
class OrderLineForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(OrderLineForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.form_show_errors = True
        self.helper.error_text_inline = False
        self.fields['product'] = forms.ModelChoiceField(
            queryset=Product.objects.all())
        self.fields['product'].required = False

After this you can filter select with jquery filters.

VelikiiNehochuha
  • 3,775
  • 2
  • 15
  • 32
  • This is what i originally had. As this is a formset factory, creating multiple forms. If i would add 20 forms in the set, it would load 20 x 1000 products in the dropdowns. Which takes a long time to load. – phicon May 24 '15 at 11:59