13

How I would dynamically create a few form fields with different questions, but the same answers?

from wtforms import Form, RadioField
from wtforms.validators import Required

class VariableForm(Form):

    def __init__(formdata=None, obj=None, prefix='', **kwargs):
        super(VariableForm, self).__init__(formdata, obj, prefix, **kwargs)
        questions = kwargs['questions']
        // How to to dynamically create three questions formatted as below?

    question = RadioField(
            # question ?,
            [Required()],
            choices = [('yes', 'Yes'), ('no', 'No')],
            )

questions = ("Do you like peas?", "Do you like tea?", "Are you nice?")  
form = VariableForm(questions = questions)
atp
  • 30,132
  • 47
  • 125
  • 187

3 Answers3

17

It was in the docs all along.

def my_view():
    class F(MyBaseForm):
        pass

    F.username = TextField('username')
    for name in iterate_some_model_dynamically():
        setattr(F, name, TextField(name.title()))

    form = F(request.POST, ...)
    # do view stuff

What I didn't realize is that the class attributes must be set before any instantiation occurs. The clarity comes from this bitbucket comment:

This is not a bug, it is by design. There are a lot of problems with adding fields to instantiated forms - For example, data comes in through the Form constructor.

If you reread the thread you link, you'll notice you need to derive the class, add fields to that, and then instantiate the new class. Typically you'll do this inside your view handler.

atp
  • 30,132
  • 47
  • 125
  • 187
  • It's not clear to me if this solution relates to my problem. I have relationship called tags in my Post model... When I calls PostForm generates tags the query displays instead of the query results. How do I run the query and send the results as comma delineated string to the Post tags field? Here is my [posted question](http://stackoverflow.com/questions/23251470/how-to-send-query-results-to-a-wtform-field). – jwogrady Apr 23 '14 at 23:21
  • 1
    By doing this way, it doesn't cleary solve the question, you couldn't separate form file like `form.py` then `a = Form(params)`, defining class inside method isn't considered as good practice? https://stackoverflow.com/questions/2583620/dynamically-create-class-attributes – TomSawyer Oct 09 '17 at 09:09
1

You're almost there:

CHOICES = [('yes', 'Yes'), ('no', 'No')]

class VariableForm(Form):

    def __new__(cls, questions, **kwargs):
        for index, question in enumerate(questions):
            field_name = "question_{}".format(index)
            field = RadioField(question,
                                  validators=[Required()],
                                  choices=CHOICES)
            setattr(cls, field_name, field)
        return super(VariableForm, cls).__new__(cls, **kwargs)
Sean Vieira
  • 155,703
  • 32
  • 311
  • 293
  • Thanks for the reply. If I do this, all the fields show up as `UnboundFields` and won't be rendered with the form. – atp Jul 24 '12 at 22:39
  • 1
    @ash - apologies - I think it needed to be `__new__` not `__init__`. I've updated my answer with new (still untested) code. Let me know if it doesn't work for you. – Sean Vieira Jul 25 '12 at 02:06
  • Thanks again for responding, unfortuantely that doesn't work (I had to do `return super(VariableForm, cls).__new__(cls, **kwargs)`. I've been looking at https://bitbucket.org/simplecodes/wtforms/src/a5f9e30260cc/wtforms/form.py but I can't tell what's going on. I'll have to keep trying other things. – atp Jul 25 '12 at 05:28
  • i am trying the same, seems better i leave my logic in forms.py and not in the view, could you create it with __new__ at the end? i am having your same problem, i know its been a while... – Raul Gomez Jan 27 '14 at 16:14
  • It doesn't work: `for name, unbound_field in itertools.chain(fields, extra_fields): TypeError: 'NoneType' object is not iterable` – TomSawyer Oct 09 '17 at 09:04
0

In my case, I used a csv and imported it using pandas.

So, this solution allows you to even use different answers if required.

data=pd.read_csv("./temp.csv")
class UserForm(Form):
    for i in data:
        if data[i][0] == 'textbox':
            formElement='TextField("%s",validators=[validators.required()], default="please add content")' %(i)

        elif data[i][0] == 'radio':
            choice = list(data[i][1:].dropna().unique().tolist())
            choiceStr=''
            for k in choice:
               choiceStr +="('"+k+"','"+k+"'),"
            
            formElement = 'RadioField("%s",validators=[validators.required()],choices=[%s], default="%s")' %(i,choiceStr, choice[0])

        elif data[i][0] == 'dropdown':
            choice = list(data[i][1:].dropna().unique().tolist())
            # choice.remove('X')
            choiceStr=''
            for k in choice:
               choiceStr +="('"+k+"','"+k+"'),"
            
            formElement = 'SelectField("%s",validators=[validators.required()],choices=[%s])' %(i,choiceStr)


        exec("%s=%s" % (i,formElement))