tags:

views:

144

answers:

5

I'm building my first form with django, and I'm seeing some behavior that I really did not expect at all. I defined a form class:

class AssignmentFilterForm(forms.Form):
filters = []
filter = forms.ChoiceField()

def __init__(self, *args, **kwargs):
 super(forms.Form, self).__init__(*args, **kwargs)
 self.filters.append(PatientFilter('All'))
 self.filters.append(PatientFilter('Assigned', 'service__isnull', False))
 self.filters.append(PatientFilter('Unassigned', 'service__isnull', True))

 for i, f in enumerate(self.filters):
  self.fields["filter"].choices.append((i, f.name))

When I output this form to a template using:

{{ form.as_p }}

I see the correct choices. However, after refreshing the page, I see the list three times in the select box. Hitting refresh again results in the list showing 10 times in the select box!

Here is my view:

@login_required
def assign_test(request):
pg = PhysicianGroup.objects.get(pk=physician_group)

if request.method == 'POST':
 form = AssignmentFilterForm(request.POST)
 if form.is_valid():
  yes = False
else:
 form = AssignmentFilterForm()
 patients = pg.allPatients().order_by('bed__room__unit', 'bed__room__order', 'bed__order' )

return render_to_response('hospitalists/assign_test.html', RequestContext(request,  {'patients': patients, 'form': form,}))

What am I doing wrong?

Thanks, Pete

+1  A: 

You're appending to the PER-CLASS variable self.filters. Make it into a PER-INSTANCE variable instead, by doing self.filters = [] at the start of __init__.

Alex Martelli
lol, thanks. New to the language. The same problem occurs because filter is static as well. How do I fix it?
slypete
Before the loop in the `__init__`, do `self.fields['filter'].choices = []` to clean up what you previously had there from "last round". Might not be inherently thread-safe, so if you're multithreading you'll need caution, but threading is a minefield for language beginners anyway, so if you can possibly avoid it you'll be much, much happier.
Alex Martelli
lol, I'm no beginner, just to this language. How can I make filter an instance var? I'm now really confused why django's form documentation is using static vars.
slypete
@slypete, fields in Django forms really need to be class vars (Django has a metaclass to deal with that!). If you're an experienced programmer http://docs.djangoproject.com/en/1.0/topics/forms/ should offer all the details you need (and I'll always be glad to help if you need some Python subtlety to help you!-)
Alex Martelli
+6  A: 

This is actually a feature of Python that catches a lot of people.

When you define variables on the class as you have with filters = [] the right half of the expression is evaluated when the class is initially defined. So when your code is first run it will create a new list in memory and return a reference to this list. As a result, each AssignmentFilterForm instance will have its own filters variable, but they will all point to this same list in memory. To solve this just move the initialization of self.filters into your __init__ method.

Most of the time you don't run into this issue because the types you are using aren't stored as a reference. Numbers, booleans, etc are stored as their value. Strings are stored by reference, but strings are immutable meaning a new string must be created in memory every time it is changed and a new reference returned.

Pointers don't present themselves often in scripting language, so it's often confusing at first when they do.

Here's a simple IDLE session example to show what's happening

>>> class Test():
    myList = []
    def __init__( self ):
     self.myList.append( "a" )


>>> Test.myList
[]
>>> test1 = Test()
>>> Test.myList
['a']
>>> test1.myList
['a']
>>> test2 = Test()
>>> test2.myList
['a', 'a']
>>> test1.myList
['a', 'a']
>>> Test.myList
['a', 'a']
spbogie
Thanks for your explanation. So why would django choose per-class vars for fields, and how do I change it?
slypete
Is there a specific page you are looking at in the documentation. In all of the Form examples I am seeing they are never using an `__init__`. I believe in this case you may actually want to eliminate the `__init__` and just do that initialization as part of the class definition.
spbogie
I think a reference to numbers, booleans, etc., are also stored, but like strings, numbers are also immutable in Python.
mipadi
A: 

As answered above, you need to initialize filters as an instance variable:

def __init__(...):
    self.filters = []
    self.filters.append(...)
    # ...

If you want to know more about how the Form class works, you should read this page in the Django wiki:

It talks about the internals of the Model class, but you'll find the general setup of fields is somewhat similar to the Form (minus the database stuff). It's a bit dated (2006), but I think the basic principles still apply. The metaclass stuff can be a bit confusing if you're new though.

ars
A: 

To clarify from some of the other answers:

The fields are, and must be, class variables. They get all sorts of things done to them by the metaclass, and this is the correct way to define them.

However, your filters variable does not need to be a class var. It can quite easily be an instance var - just remove the definition from the class and put it in __init__. Or, perhaps even better, don't make it a property at all - just a local var within __init__. Then, instead of appending to filters.choices, just reassign it.

def __init__(self, *args, **kwargs):
        super(forms.Form, self).__init__(*args, **kwargs)
        filters = []
        filters.append(PatientFilter('All'))
        filters.append(PatientFilter('Assigned', 'service__isnull', False))
        filters.append(PatientFilter('Unassigned', 'service__isnull', True))

        self.fields["filter"].choices = [(i, f.name) for i, f in enumerate(filters)]
Daniel Roseman
+1  A: 

I picked up the book Pro Django which answers this question. It's a great book by the way, and I highly recommend it!

The solution is to make BOTH the choice field and my helper var both instance variables:

class AssignmentFilterForm(forms.Form):
def __init__(self, pg, request = None):
 super(forms.Form, self).__init__(request)
 self.filters = []

 self.filters.append(PatientFilter('All'))
 self.filters.append(PatientFilter('Assigned', 'service__isnull', False))
 self.filters.append(PatientFilter('Unassigned', 'service__isnull', True))
 self.addPhysicians(pg)

 self.fields['filter'] = forms.ChoiceField()
 for i, f in enumerate(self.filters):
  self.fields['filter'].choices.append((i, f.name))

Clearing out the choices works but would surely result in threading issues.

slypete