views:

801

answers:

3

I have some models and I want to generate a multi-selection form from this data. So the form would contain an entry for each category and the choices would be the skills in that category.

models.py

class SkillCategory(models.Model):
    name = models.CharField(max_length=50)

class Skill(models.Model):
    name = models.CharField(max_length=50)
    category = models.ForeignKey(SkillCategory)

Is there a way to auto-generate the form fields? I know I can manually add a 'SkillCategory' entry in the form for each SkillCategory, but the reason to have it as a model is so skills and skillcategories can be edited freely.

I want to do something like this: (I tried this, but didn't get it to work, don't remember the exact error...)

forms.py

class SkillSelectionForm(forms.Form):
    def __init__(*args, **kwargs):
        super(SkillSelectionForm, self).__init__(*args, **kwargs)
        for c in SkillCategory.objects.all():
            category_skills = [(pk, s.name) for s in c.skill_set.all()]
            setattr(self, c.name, forms.MultipleChoiceField(choices=category_skills, widget=forms.CheckboxSelectMultiple))


SOLUTION

This creates a form field entry using the SkillCategory.name and assigns choices as those in Skill. field_name/display_name are used to avoid issues with non-ascii category names.

forms.py

def get_categorized_skills():
    skills = {}
    for s in Skill.objects.values('pk', 'name', 'category__name').order_by('category__name'):
        if s['category__name'] not in skills.keys():
            skills[s['category__name']] = []
        skills[s['category__name']].append((s['pk'], s['name']))
    return skills

class SkillSelectionForm(forms.Form): 
    def __init__(self, *args, **kwargs):
        super(SkillSelectionForm, self).__init__(*args, **kwargs)
        skills = get_categorized_skills()
        for idx, cat in enumerate(skills.keys()):
            field_name = u'category-{0}'.format(idx)
            display_name = cat
            self.fields[field_name] = forms.MultipleChoiceField(choices=skills[cat], widget=forms.CheckboxSelectMultiple, label=display_name)
+1  A: 

What you want is a Formset. This will give you a set of rows, each of which maps to a specific Skill.

See the Formset documentation and the page specifically on generating formsets for models.

Daniel Roseman
Thanks, this appears to be the right approach, I just need to re-think the problem from this perspective.
monkut
+2  A: 

Okay so you can't set fields like that on forms.Form, for reasons which will become apparent when you see DeclarativeFieldsMetaclass, the metaclass of forms.Form (but not of forms.BaseForm). A solution which may be overkill in your case but an example of how dynamic form construction can be done, is something like this:

base_fields = [
    forms.MultipleChoiceField(choices=[
        (pk, s.name) for s in c.skill_set.all()
    ]) for c in SkillCategory.objects.all()
]
SkillSelectionForm = type('SkillSelectionForm', (forms.BaseForm,), {'base_fields': base_fields})
ozan
Funky, but looks workable. Thanks for the code reference!
monkut
+2  A: 

Take a look at creating dynamic forms in Django, from b-list.org and uswaretech.com. I've had success using these examples to dynamically create form content from models.

John Paulett
Thanks, this pointed me in the right direction, see my edit above for my final solution.
monkut