views:

2069

answers:

6

I'm using Django forms in my website and would like to control the order of the fields.

Here's how I define my forms:

class edit_form(forms.Form):
    summary = forms.CharField()
    description = forms.CharField(widget=forms.TextArea)


class create_form(edit_form):
    name = forms.CharField()

The name is immutable and should only be listed when the entity is created. I use inheritance to add consistency and DRY principles. What happens which is not erroneous, in fact totally expected, is that the name field is listed last in the view/html but I'd like the name field to be on top of summary and description. I do realize that I could easily fix it by copying summary and description into create_form and loose the inheritance but I'd like to know if this is possible.

Why? Imagine you've got 100 fields in edit_form and have to add 10 fields on the top in create_form - copying and maintaining the two forms wouldn't look so sexy then. (This is not my case, I'm just making up an example)

So, how can I override this behavior?

Edit:

Apparently there's no proper way to do this without going through nasty hacks (fiddling with .field attribute). The .field attribute is a SortedDict (one of Django's internal datastructures) which doesn't provide any way to reorder key:value pairs. It does how-ever provide a way to insert items at a given index but that would move the items from the class members and into the constructor. This method would work, but make the code less readable. The only other way I see fit is to modify the framework itself which is less-than-optimal in most situations.

In short the code would become something like this:

class edit_form(forms.Form):
    summary = forms.CharField()
    description = forms.CharField(widget=forms.TextArea)


class create_form(edit_form):
    def __init__(self,*args,**kwargs):
        forms.Form.__init__(self,*args,**kwargs)

        self.fields.insert(0,'name',forms.CharField())

That shut me up :)

+3  A: 

See the notes in this SO question on the way Django's internals keep track of field order; the answers include suggestions on how to "reorder" fields to your liking (in the end it boils down to messing with the .fields attribute).

Alex Martelli
A: 

I was doing it backwards. It's a lot cleaner to inherit create_form with the name field and then removing it in the edit_form.

Here's my solution:

class create_form(forms.Form):
    name = forms.CharField()
    summary = forms.CharFied()
    description = forms.CharField(widget=forms.TextArea)

''' Inherits create_form and adds/removes certain fields '''
class edit_form(create_form):
    newfield = forms.CharField() # Only displayed when editing 

    def __init__(self,*args,**kwargs):
        create_form.__init__(self,*args,**kwargs)

         del self.fields['name'] # don't show the name field when editing
Hannson
In most cases this doesn't make sense, e.g. when subclassing a form defined in a reusable app.
akaihola
+14  A: 

I had this same problem and I found another technique for reordering fields in the Django CookBook:

class edit_form(forms.Form):
    summary = forms.CharField()
    description = forms.CharField(widget=forms.TextArea)


class create_form(edit_form):
    name = forms.CharField()

    def __init__(self, *args, **kwargs):
        super(edit_form, self).__init__(*args, **kwargs)
        self.fields.keyOrder = ['name', 'summary', 'description']
Selene
Interesting... could work
Hannson
It seems to work, and proves wrong the assumption that SortedDict "doesn't provide any way to reorder key:value pairs".
akaihola
A: 

Alternate methods for changing the field order:

Pop-and-insert:

self.fields.insert(0, 'name', self.fields.pop('name'))

Pop-and-append:

self.fields['summary'] = self.fields.pop('summary')
self.fields['description'] = self.fields.pop('description')

Pop-and-append-all:

for key in ('name', 'summary', 'description'):
    self.fields[key] = self.fields.pop(key)

Ordered-copy:

self.fields = SortedDict( [ (key, self.fields[key])
                            for key in ('name', 'summary' ,'description') ] )

But Selene's approach from the Django CookBook still feels clearest of all.

akaihola
+1  A: 

Like Selenes answer, just a short comment - guess it is a typo and that the call to super should be:

super(create_form, self).__init__(*args, **kwargs)

and not:

super(edit_form, self).__init__(*args, **kwargs)
jenlu
+3  A: 

I used the solution posted by Selene but found that it removed all fields which weren't assigned to keyOrder. The form that I'm subclassing has a lot of fields so this didn't work very well for me. I coded up this function to solve the problem using akaihola's answer, but if you want it to work like Selene's all you need to do is set throw_away to True.

def order_fields(form, field_list, throw_away=False):
    """
    Accepts a form and a list of dictionary keys which map to the
    form's fields. After running the form's fields list will begin
    with the fields in field_list. If throw_away is set to true only
    the fields in the field_list will remain in the form.

    example use:
    field_list = ['first_name', 'last_name']
    order_fields(self, field_list)
    """
    if throw_away:
        form.fields.keyOrder = field_list
    else:
        for field in field_list[::-1]:
            form.fields.insert(0, field, form.fields.pop(field))

This is how I'm using it in my own code:

class NestableCommentForm(ExtendedCommentSecurityForm):
    # TODO: Have min and max length be determined through settings.
    comment = forms.CharField(widget=forms.Textarea, max_length=100)
    parent_id = forms.IntegerField(widget=forms.HiddenInput, required=False)

    def __init__(self, *args, **kwargs):
        super(NestableCommentForm, self).__init__(*args, **kwargs)
        order_fields(self, ['comment', 'captcha'])
Joshua
I really like this! Thanks.
slack3r