Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WTForm: FieldList with SelectField, how do I render?

I have this order form that allows my users to create an order. An order consists of multiple tuples of (producetype, quantity). Producetype should be rendered in a <select> form while quantity can just be an input. The choices of producetype should be dynamically added because that could change. Currently, I've written this in bare html

enter image description here

I would like to use WTForm for this because WTForm really simplifies my code. However, I am unable to do so:

Code:

class OrderEntryForm(Form):
  quantity = IntegerField('Quantity',
                          [validators.Required(), validators.NumberRange(min=1)])
  # we will be dynamically adding choices
  producetype = SelectField('Produce',
                            [validators.Required()],
                            choices=[])

class OrderForm(Form):
  name = TextField('Crop', [validators.Length(min=3, max=60)])
  due_date = DateField('Due Date', [validators.required()])
  order_entries = FieldList(FormField(OrderEntryForm))

I have the following questions:

  1. How can I dynamically add choices to the order_entries field of the OrderForm?
  2. If I have an order, order = { name:hello, due_date:2014-06-18, order_entries:[ {producetype_id: 1, quantity: 2}, {producetype_id: 3, quantity: 4}] }, how can populate my OrderForm with the right OrderEntryForm values?
like image 998
disappearedng Avatar asked Jun 19 '14 00:06

disappearedng


2 Answers

For anyone else stumped by SelectFields + FieldLists, this is how I implemented a FieldList with SelectFields (inspired by nsfyn55's answer). This approach dynamically renders an arbitrary number of SelectFields to the /home route based on JSON data.

The route (routes.py):

@app.route('/home', methods=['POST', 'GET'])
def home():
    custom_metadata = data

    select_metadata_form_list = SelectFormList()
    select_metadata_form_list.select_entries = get_select_entries()

    context = {
        "select_metadata_form_list": select_metadata_form_list,
    }

    return render_template('home.html', **context)

The forms (forms.py):

class SelectForm(FlaskForm):
    select = SelectField("Placeholder", choices=[])


class SelectFormList(FlaskForm):
    select_entries = FieldList(FormField(SelectForm))

The template (home.html):

{% for select_form in select_metadata_form_list.select_entries %}
    {{select_form.select.label}}: {{ select_form.select}}
{% endfor %}

Helper methods (app.py):

def get_select_entries():
    """
    Converts custom metadata to a forms.SelectForm(), which can then be
    used by SelectFormlist() to dynamically render select items.

    :return: <forms.SelectForm object>
    """
    select_data = get_select_data_from_custom_metadata()

    select_data_labeled = get_labled_select_data(select_data=select_data)

    all_select_items = []
    for select_dict in select_data_labeled:
        for k, v in select_dict.items():
            select_id = uuid.uuid1()   # allows for multiple selects
            select_entry = SelectForm()
            select_entry.select.label = k
            select_entry.id = select_id
            select_entry.select.choices = v
            all_select_items.append(select_entry)

    return all_select_items
def get_select_data_from_custom_metadata():
    """
    [
    {"Seniority": ["Intern", "Associate", "Senior"]}
    ]
    :return: List of dictionaries containing key and list of select values
    """
    type = "select"
    select_data = []
    custom_metadata = get_custom_metadata()

    for field in custom_metadata["fields"]:
        if field["type"] == type:
            select_data.append({field["key"]: field["data_list"]})

    return select_data

The 'data' (custom_metadata.json):

{
  "fields": [
    {
      "type": "text",
      "key": "Position"
    },

    {
      "type": "select",
      "key": "Seniority",
      "data_list": ["Intern", "Associate", "Senior", "Executive"]
    },
    {
      "type": "select",
      "key": "Company",
      "data_list": ["Ford", "Chevy", "Toyota"]
    },
    {
      "type": "select",
      "key": "Team",
      "data_list": ["Bucks", "Raptors", "Thunder"]
    }
  ]
}

The result:

enter image description here

like image 57
Ty Hitzeman Avatar answered Oct 15 '22 05:10

Ty Hitzeman


How can I dynamically add choices to the order_entries field of the OrderForm

This depends on what you mean

If you mean you want to add lines items to the form after render. You have to use DOM manipulation to add these on the client side. WTForms has a naming convention for addressing indices on form fields. Its just name="<form-field-name>-<index>". If you add a name using javascript that follows this convention WTForms will know how to handle it on the backend.

If you mean you want to have a dynamic choice list then you can just iterate over the FieldList in the form after its instantiated. You can assign choices any iterable of 2-tuples. In the example below I am assigning them directly but they could just as easily have been retrieved from storage.

order_form = OrderForm()
for sub_form in order_form.order_entries:
    sub_form.producetype.choices = [('2', 'apples'), ('2', 'oranges')]

how can populate my OrderForm with the right OrderEntryForm values?

You can just bind objects directly to the form using the obj keyword argument. WTForms is smart enough to dynamically build the form from the object's attributes.

from wtforms import Form, IntegerField, SelectField, TextField, FieldList, FormField
from wtforms import validators
from collections import namedtuple

OrderEntry = namedtuple('OrderEntry', ['quantity', 'producetype'])
Order = namedtuple('Order', ['name', 'order_entries'])

class OrderEntryForm(Form):
  quantity = IntegerField('Quantity',
                          [validators.Required(), validators.NumberRange(min=1)])
  # we will be dynamically adding choices
  producetype = SelectField('Produce',
                            [validators.Required()],
                            choices=[
                                (1, 'carrots'),
                                (2, 'turnips'),
                            ])

class OrderForm(Form):
  name = TextField('Crop', [validators.Length(min=3, max=60)])
  order_entries = FieldList(FormField(OrderEntryForm))

# Test Print of just the OrderEntryForm
o_form = OrderEntryForm()
print o_form.producetype()

# Create a test order
order_entry_1 = OrderEntry(4, 1)
order_entry_2 = OrderEntry(2, 2)

order = Order('My First Order', [order_entry_1, order_entry_2])

order_form = OrderForm(obj=order)

print order_form.name
print order_form.order_entries

The above example creates a sample Order and supplies it to the obj keyword. On render this will generate the following(unstyled):

enter image description here

like image 37
nsfyn55 Avatar answered Oct 15 '22 06:10

nsfyn55