0

I am implementing nested inlines with my 3-tiered model and currently having it functional. However, I can't limit my relevantindicator drop-down choices even though I am passing the correct instance. Currently relevantindicator displays all values in the table in the drop-down selection. I'd like to limit the values to only those associated with the disease instance. Is there a way to do that?

I'm using Correct way to save nested formsets in Django and http://yergler.net/blog/2009/09/27/nested-formsets-with-django/ as references.

models.py

class Disease(models.Model):
    disease = models.CharField(max_length=255)

class Indicator(models.Model):
    relevantdisease = models.ForeignKey(Disease)       
    indicator = models.CharField(max_length=255)

class IndicatorValue(models.Model):
    relevantindicator = models.ForeignKey(Indicator)
    indicator_value = models.CharField(max_length=50)

forms.py

class BaseIndicatorFormSet(BaseInlineFormSet):

     def __init__(self, *args, **kwargs):
        try:
            instance = kwargs.pop('instance')
        except KeyError:
            super(BaseIndicatorFormSet, self).__init__(*args, **kwargs)

     def save_new(self, form, commit=True):
        instance = super(BaseIndicatorFormSet, self).save_new(form, commit=commit)

        form.instance = instance

        for nested in form.nested:
            nested.instance = instance

        for cd in nested.cleaned_data:
            cd[nested.fk.name]=instance

        return instance
...

     def add_fields(self,form,index):
        super(BaseIndicatorFormSet, self).add_fields(form, index)

        try:
            instance = self.get_queryset()[index]
            pk_value = instance.pk

        except IndexError:
            instance=None
            pk_value = hash(form.prefix)

        form.nested = [
            IndicatorValueFormSet(
                disease = instance,
                queryset = IndicatorValue.objects.filter(relevantindicator = pk_value), 
                prefix = 'value_%s' % pk_value)]


class BaseIndicatorValueFormSet(BaseModelFormSet):

    def __init__(self, disease, *args, **kwargs):
        super(BaseIndicatorValueFormSet, self).__init__(*args, **kwargs)
        self.disease = disease     

    def save_new(self, form, commit=True):
        instance = form.save(commit=False)
        instance.disease = self.disease
        if commit:
           instance.save()
        return instance

    def save_existing(self, form, instance, commit=True):
        return self.save_new(form, commit)

class IndicatorValueForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        try:
            disease_obj = kwargs.pop('disease')
        except KeyError:
            super(IndicatorValueForm, self).__init__(*args, **kwargs)
            return

        super(IndicatorValueForm, self).__init__(*args, **kwargs)
        queryset = Indicator.objects.filter(relevantdisease =disease_obj)
        self.fields['relevantindicator'].queryset = queryset


disease_obj = get_object_or_404(Disease, pk=2) #hard-wired
CurriedForm = formset_factory(IndicatorValueForm, extra=3)
CurriedForm.form = staticmethod(curry(IndicatorValueForm, disease = disease_obj))
IndicatorValueFormSet = inlineformset_factory(Indicator, IndicatorValue,   formset=BaseIndicatorValueFormSet, form = CurriedForm, extra=3)
IndicatorFormSet = inlineformset_factory(Disease, Indicator, formset=BaseIndicatorFormSet, extra=0)

views.py

 disease = get_object_or_404(Disease, pk=disease_id)

 if request.method == "POST":
      formset = IndicatorFormSet(request.POST, instance=disease)

    if formset.is_valid():
       rooms = formset.save_all()
       return HttpResponseRedirect(reverse('option', kwargs={'disease_id':disease_id}))
 else:
       formset = IndicatorFormSet(instance=disease)

context = {'disease': disease, 'indicators': formset, 'hide_breadcrumb':hide_breadcrumb}
   return render_to_response('valdrui.html',context, context_instance=RequestContext(request))

template.html

  {% if relevantindicator.nested %}
  {% for formset in relevantindicator.nested %}
  {{ formset.as_table }}
  {% endfor %}
  {% endif %}

Update My feeling is that I need to pass the disease instance from form.nested down to BaseIndicatorValueFormSet. But it does not seem to be working.

Screenshots to provide clarity.

relevantindicator provides a drop-down

relevantindicator provides a drop-down

When there is a indicator_value, the correct relevantindicator is selected. However, when adding a new indicator_vale, all relevantindicator for all relevantdiseases are available. I'd like to limit the relevantindicator choices to the relevantdiseases (the disease instance)

When there is a indicator_value, the correct relevantindicator is selected. However, when adding a new indicator_value, all relevantindicator for all relevantdiseases are available. I'd like to limit the relevantindicator choices to the relevantdiseases (the disease instance)

Update 2: I had to define an instance in BaseIndicatorFormSet under def __init__. I also needed to define the forms in forms.py since BaseIndicatorFormSet calls IndicatorValueFormSet. The instance is currently hard-wired, just to see if the template will render. Unfortunately, the form in the template does not render but also does not produce any errors. I can't figure out why since there is no error produced.

Community
  • 1
  • 1
nlr25
  • 1,605
  • 5
  • 17
  • 27
  • is it in the `IndicatorValueFormSet` where you want to limit `relevantindicator` choices to those which match the `disease` passed as instance to `IndicatorFormSet`? – Anentropic Aug 30 '13 at 13:21
  • Yes I believe so. When entering values in the `IndicatorValueFormSet` there is a FK drop-down that corresponds to the value being entered by the user. This drop-down is in the form.nested code. However, it shows all `relevantindicator` for all diseases. – nlr25 Aug 30 '13 at 16:08
  • How can indicator be a drop-down? It's a CharField. From your code it looks like relevantdisease and relevantindicator will be drop downs, as those are the FK's. Can you post a snapshot of your form in the browser, indicating the field you wish to filter on – professorDante Sep 03 '13 at 17:51
  • You're absolutely correct. The relevantindicator is the drop-down. I made the changes in the OP. I also added snapshots of my form with explanations. I'd like to filter on relevantindicator for the disease instance I pass. – nlr25 Sep 03 '13 at 18:13

2 Answers2

0

here's a complete forms.py you can base your code on

from django import forms
from django.forms.formsets import DELETION_FIELD_NAME
from django.forms.models import BaseInlineFormSet, BaseModelFormSet, inlineformset_factory

from .models import Disease, Indicator, IndicatorValue


class BaseIndicatorFormSet(BaseInlineFormSet):

    def save_new(self, form, commit=True):
        instance = super(BaseIndicatorFormSet, self).save_new(form, commit=commit)
        form.instance = instance

        for nested in form.nested:
            nested.instance = instance

        for cd in nested.cleaned_data:
            cd[nested.fk.name]=instance

        return instance

    def add_fields(self, form, index):
        super(BaseIndicatorFormSet, self).add_fields(form, index)

        try:
            instance = self.get_queryset()[index]
            pk_value = instance.pk
        except IndexError:
            instance=None
            pk_value = hash(form.prefix)

        form.nested = [
            IndicatorValueFormSet(
                instance=instance,
                disease=instance.relevantdisease,
                prefix='value_%s' % pk_value,
                queryset=IndicatorValue.objects.filter(relevantindicator=pk_value),
            )
        ]

    def should_delete(self, form):
        """
        Convenience method for determining if the form's object will
        be deleted; cribbed from BaseModelFormSet.save_existing_objects.
        """

        if self.can_delete:
            raw_delete_value = form._raw_value(DELETION_FIELD_NAME)
            should_delete = form.fields[DELETION_FIELD_NAME].clean(raw_delete_value)
            return should_delete

        return False

    def save_all(self, commit=True):
        """
        Save all formsets and along with their nested formsets.
        """

        # Save without committing (so self.saved_forms is populated)
        # - We need self.saved_forms so we can go back and access
        #    the nested formsets
        objects = self.save(commit=False)

        # Save each instance if commit=True
        if commit:
            for o in objects:
                o.save()

        # save many to many fields if needed
        if not commit:
            self.save_m2m()

        # save the nested formsets
        for form in set(self.initial_forms + self.saved_forms):
            if self.should_delete(form): continue

            for nested in form.nested:
                nested.save(commit=commit)


class BaseIndicatorValueFormSet(BaseInlineFormSet):

    def __init__(self, disease, *args, **kwargs):
        self.disease = disease
        super(BaseIndicatorValueFormSet, self).__init__(*args, **kwargs)

    def _construct_form(self, i, **kwargs):
        form = super(BaseIndicatorValueFormSet, self)._construct_form(i, **kwargs)
        form.fields['relevantindicator'].parent_instance = self.disease
        return form

    def save_new(self, form, commit=True):
        instance = form.save(commit=False)
        instance.disease = self.disease
        if commit:
           instance.save()
        return instance

    def save_existing(self, form, instance, commit=True):
        return self.save_new(form, commit)


IndicatorValueFormSet = inlineformset_factory(
    Indicator,
    IndicatorValue,
    formset=BaseIndicatorValueFormSet,
    extra=3
)

IndicatorFormSet = inlineformset_factory(
    Disease,
    Indicator,
    formset=BaseIndicatorFormSet,
    extra=0
)
Anentropic
  • 32,188
  • 12
  • 99
  • 147
  • I get a "__init__() takes at least 2 arguments (1 given)" error. I believe the disease parameter is not passed since instance=disease from views.py is passed to the IndicatorFormSet not the IndicatorValueFormSet. – nlr25 Aug 30 '13 at 16:09
  • I updated the code in your question to use `disease` var name instead of `result` as was copy and pasted from the other SO question... maybe it's that? – Anentropic Aug 30 '13 at 16:10
  • Oh I didn't notice that edit. After implementing your changes I get a "'IndicatorValueFormFormSet' object has no attribute 'fields'" error. – nlr25 Aug 30 '13 at 16:16
  • ah sorry, my bad for doing it off top of my head... [edited] how about now? – Anentropic Aug 30 '13 at 16:24
  • Now I get a "global name 'disease' is not defined". Close I think. Appreciating the help for sure. – nlr25 Aug 30 '13 at 16:27
  • oops... how about now? – Anentropic Aug 30 '13 at 16:29
  • now that brought me to "'IndicatorValueFormFormSet' object has no attribute 'disease'" error. – nlr25 Aug 30 '13 at 16:31
  • The exception was raised on this line "form.fields['relevantindicator'].queryset = existing_qs.filter(relevantdisease=self.disease)" – nlr25 Aug 30 '13 at 16:35
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/36560/discussion-between-anentropic-and-newtothis) – Anentropic Aug 30 '13 at 16:41
0

Try making a custom ModelForm to filter your relevantindicator field:

class IndicatorValueForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        try:
            disease_obj = kwargs.pop('disease')
        except KeyError:
            super(IndicatorValueForm, self).__init__(*args, **kwargs)
            return
        super(IndicatorValueForm, self).__init__(*args, **kwargs)
        queryset = Indicator.objects.filter(relevantdisease =disease_obj)
        self.fields['relevantindictor'].queryset = queryset

This amends the Forms init method with the disease object you wish to filter on, adding it to the kwargs.

To use it, we need to pass in the Disease object into the form with the curry method:

disease_obj = <your-disease-instance>
CurriedForm = staticmethod(curry(IndicatorValueForm, disease = disease_obj))
IndicatorValueFormSet = inlineformset_factory(Indicator, IndicatorValue,   formset=BaseIndicatorValueFormSet, form = CurriedForm, extra=3)

Now your drop down should only show the indicators filtered by disease.

professorDante
  • 2,290
  • 16
  • 26
  • Does this replace my IndicatorFormSet? I'll give this a try and let you know. Stay tuned! – nlr25 Sep 03 '13 at 19:41
  • No it doesn't, it's a Form to display the IndicatorValueModel, used in the inline_formset IndicatorValueFormSet. From the screenshots, that seemed to be the offending Model. – professorDante Sep 03 '13 at 19:51
  • I get a "name 'self' is not defined" error. I updated the original post with the updated code you wrote above. I put everything in my forms.py and left the views.py and template the same. Let me know if the location of the code is suitable. – nlr25 Sep 03 '13 at 22:37
  • You've not indented your new code correctly - indenting is crucial to Python. Compiler now thinks that __init__ is a standalone function, so it barfs about 'self'. It would help if I had done the same. :-)) Edited my answer, try that. – professorDante Sep 03 '13 at 22:46
  • You're right, sorry about that. I'm still new. I'm getting a "name 'instance' is not defined" error which means that passing my disease instance is not working. Is there a way to pass the disease instance from my views.py to form.py? Or is it easier to put the disease_obj, curry, and inlineformset_factory in views.py? I'll have to play with this sometime tomorrow..so stay tuned! – nlr25 Sep 04 '13 at 00:40
  • No problems in being a newbie, we all started at the beginning, but don't use SO as a crutch for learning Python correctly. You need to know fundamentals of Python and OO, this code is reasonably complex. Your new bug is again because you haven't indented your code disease_obj = instance.relevantdisease at the bottom of Forms.py. The disease instance you need only exists inside the ModelForm class. Move all that code to your view, as that's where it shoud be, so you can use the Disease instance you get in there. – professorDante Sep 04 '13 at 01:34
  • I get this "issubclass() arg 1 must be a class" error now but can't find too many reasons why this pops up. I'll update my code above and add a traceback. I'll play around with it a bit more to trouble shoot as well. Thanks! – nlr25 Sep 04 '13 at 16:00
  • not sure if you're still interested in helping. But I got the `issubclass()` error fixed. I also had to define an `instance` in BaseIndicatorFormSet under `def __init__`. Last thing is that I need to define the forms in forms.py since `BaseIndicatorFormSet` calls `IndicatorValueFormSet`. The instance is currently hard-wired, just to see if the template will render. Unfortunately, the form in the template does not render but also does not produce any errors. I can't figure out why since there is no error produced. – nlr25 Sep 05 '13 at 07:09
  • I think my advice at this point would be to put this on hold and go and learn about the fundamentals of Django and Python. I can see at least 4 other issues with the current codebase. I help you with one issue and it just presents another. Wihout understanding classes and __init__ methods etc, you will not fix this issue. Some current bugs I can see: IndicatorValueForm __init__ not indented, creating Formsets in forms.py and views.py, IndicatorValueFormSet is never used in views.py, and indicators is the template formset variable, not relevantindicators.nested, causing the empty template. – professorDante Sep 05 '13 at 16:21
  • thanks, fair enough. This code is rather complex to me compared to some more standard implementations I've done in the past. Thanks for your help though. – nlr25 Sep 05 '13 at 16:32
  • To answer some of your concerns for future readers, the first 2 concerns were fixed in the post (they were just typos when posting). IndicatorValueFormSet is never used in views.py because it is called in forms.py under form.nested. The last concern about the template is 1 thing I don't understand and will need to figure out. Thanks! – nlr25 Sep 05 '13 at 16:52