views:

153

answers:

3

I guess it's more of a python question than a django one, but I couldn't replicate this behavior anywhere else, so I'll use exact code that doesn't work as expected.

I was working on some dynamic forms in django, when I found this factory function snippet:

def get_employee_form(employee):
    """Return the form for a specific Board."""
    employee_fields = EmployeeFieldModel.objects.filter(employee = employee).order_by   ('order')
    class EmployeeForm(forms.Form):
        def __init__(self, *args, **kwargs):
            forms.Form.__init__(self, *args, **kwargs)
            self.employee = employee
        def save(self):
            "Do the save"
    for field in employee_fields:
        setattr(EmployeeForm, field.name, copy(type_mapping[field.type]))
    return type('EmployeeForm', (forms.Form, ), dict(EmployeeForm.__dict__))

[from :http://uswaretech.com/blog/2008/10/dynamic-forms-with-django/]

And there's one thing that I don't understand, why returning modified EmployeeForm doesn't do the trick? I mean something like this:

def get_employee_form(employee):
    #[...]same function body as before

    for field in employee_fields:
        setattr(EmployeeForm, field.name, copy(type_mapping[field.type]))
    return EmployeeForm

When I tried returning modified class django ignored my additional fields, but returning type()'s result works perfectly.

+3  A: 

I just tried this with straight non-django classes and it worked. So it's not a Python issue, but a Django issue.

And in this case (although I'm not 100% sure), it's a question of what the Form class does during class creation. I think it has a meta class, and that this meta class will finalize the form initialization during class creation. That means that any fields you add after class creation will be ignored.

Therefore you need to create a new class, as is done with the type() statement, so that the class creation code of the meta class is involved, now with the new fields.

Lennart Regebro
+5  A: 

Lennart's hypothesis is correct: a metaclass is indeed the culprit. No need to guess, just look at the sources: the metaclass is DeclarativeFieldsMetaclass currently at line 53 of that file, and adds attributes base_fields and possibly media based on what attributes the class has at creation time. At line 329 ff you see:

class Form(BaseForm):
    "A collection of Fields, plus their associated data."
    # This is a separate class from BaseForm in order to abstract the way
    # self.fields is specified. This class (Form) is the one that does the
    # fancy metaclass stuff purely for the semantic sugar -- it allows one
    # to define a form using declarative syntax.
    # BaseForm itself has no way of designating self.fields.
    __metaclass__ = DeclarativeFieldsMetaclass

This implies there's some fragility in creating a new class with base type -- the supplied black magic might or might not carry through! A more solid approach is to use the type of EmployeeForm which will pick up any metaclass that may be involved -- i.e.:

return type(EmployeeForm)('EmployeeForm', (forms.Form, ), EmployeeForm.__dict__)

(no need to copy that __dict__, btw). The difference is subtle but important: rather than using directly type's 3-args form, we use the 1-arg form to pick up the type (i.e., the metaclass) of the form class, then call THAT metaclass in the 3-args form.

Blackly magicallish indeed, but then that's the downside of frameworks which do such use of "fancy metaclass stuff purely for the semantic sugar" &c: you're in clover as long as you want to do exactly what the framework supports, but to get out of that support even a little bit may require countervailing wizardry (which goes some way towards explaining why often I'd rather use a lightweight, transparent setup, such as werkzeug, rather than a framework that ladles magic upon me like Rails or Django do: my mastery of deep black magic does NOT mean I'm happy to have to USE it in plain production code... but, that's another discussion;-).

Alex Martelli
I think the reason for dict(EmployeeForm.__dict__) was that earlier forms.Form used to return a DictProxy not a dict.
uswaretech
+1  A: 

It's worth noting that this code snippet is a very poor means to the desired end, and involves a common misunderstanding about Django Form objects - that a Form object should map one-to-one with an HTML form. The correct way to do something like this (which doesn't require messing with any metaclass magic) is to use multiple Form objects and an inline formset.

Or, if for some odd reason you really want to keep things in a single Form object, just manipulate self.fields in the Form's __init__ method.

Carl Meyer
formset_factory creates multiple instances of same form and saves it transparently. This example is displaying different fields on the form, based on employee department. I am not sure I understand what you mean when you say that former can be used for the latter.Can you elaborate? Thanks in advance.
Lakshman Prasad
If your intent is is to show a html form which allows editing the names of all employees of a dept, inline formset is what you want. However, if fro some dept, you want to show a commission, and not for others, this is the way to do it. Both are orthogonal, and can be matched together.
uswaretech