Home > Net >  Flask create form with SelectField for every item in a list
Flask create form with SelectField for every item in a list

Time:03-02

I am new to Flask and i'm trying to make something that requires some skills I am not able to find an answer to when searching SO.

So, the concept is... I have a list of songs that is generated dynamically. Basically we don't know how many there will be. The songs are stored in a list within a list, like this:

[[id,artist_name,track_name],[id,artist_name,track_name],[id,artist_name,track_name] etc]

I would like to create a form with a SelectField for every list in the list so that the user can give scores to every song in the list.

The route passing on the list item looks like this:

@app.route('/submitlist', methods=['GET', 'POST'])
def submitlist():
    form = forms.Playlist()

    if request.method == 'POST':
        if form.validate():
            song_data = [[id,artist_name,track_name],[id,artist_name,track_name]...]
            session['thesongs'] = song_data
            return redirect(url_for('songs'))

    return render_template('songs.html', form=form)

The route receiving the list looks like this:

@app.route('/songs', methods=['GET', 'POST'])
def songs():
    form = forms.SongsRated()

    if request.method == 'POST':
        data = form.rating.data
        session['results'] = data
        return redirect(url_for('results'))

    return render_template('songs.html', thesongs=session['thesongs'], form=form)

What I am unable to figure out is what the form SongsRated should look like as is should return a dynamic number of SelectFields. I should also be able to collect the return value and identify which SelectField value belongs to which item in the list (thesongs).

And finally I would like to do some verification as I would only like the user be able to score 10 songs (no matter how many) and the scores should all be unique (1-10).

I am sorry if this wasn't explained very well.

Thanks in advance.

CodePudding user response:

You can find an explanation of how to create a form dynamically within your view in this tutorial.
Based on that I wrote you the following example.
A form is created with a select field for each item within the list. If the form is submitted, it will provide you with the id and the selected value of the entry for all assigned rankings. The identification is based on the field name, as this contains the original id of the entry.
Double selection is prevented by a custom validator.
In addition, a JavaScript prevents a double selection from happening at all. For this purpose, a listener for change-events is added to each SelectField, which disables the selected ranking for all others or enables it again if a different ranking is selected.

Flask (app.py)
from flask import (
    Flask,
    render_template,
    request
)
from flask_wtf import FlaskForm
from wtforms import SelectField
from wtforms.validators import ValidationError

app = Flask(__name__)
app.secret_key = 'your secret here'

# Check if a ranking was selected twice.
def validate_rating(form, field):
    if field.data:
        for _f in form:
            if _f != field and _f.data == field.data:
                raise ValidationError('A rating can only be given once.')

class SongsRated(FlaskForm):
    pass

songs = [(i, f'Unknown Artist {i}', f'Untitled Track {i}') for i in range(1,16)]

@app.route('/', methods=['GET', 'POST'])
def index():
    # Generate the actual form.
    F = SongsRated
    for id,artist,track in songs:
        field = SelectField(
            f'{artist} - {track}',
            [validate_rating],
            choices=[(i if i > 0 else '',i) for i in range(11)],
            validate_choice=False
        )
        setattr(F, f'track-{id}', field)

    # Create an instance of the form.
    form = F(request.form)
    # Once the form has been received and the entry is valid, ...
    if form.validate_on_submit():
        # ... inquire about the rankings awarded.
        id_values = [
            (int(f.name[6:]), int(f.data)) for f in form \
            if f.name.startswith('track') and f.data
        ]

    return render_template('index.html', **locals())
HTML (templates/index.html)
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Index</title>
  </head>
  <body>
    <form method="post">
      {{ form.csrf_token }}
      {% for field in form -%}
      {% if field.name.startswith('track') -%}
      <div>
        {{ field.label() }}
        {{ field() }}
        {% if field.errors -%}
        <ul>
          {% for error in field.errors -%}
          <li>{{ error }}</li>
          {% endfor -%}
        </ul>
        {% endif -%}
      </div>
      {% endif -%}
      {% endfor -%}
      <input type="submit" />
    </form>

    {% if id_values -%}
    <output>{{ id_values }}</output>
    {% endif -%}

    <script type="text/javascript">
      /* This script is optional and not strictly required. */

      (() => {
        
        const elems = document.querySelectorAll('select[name^="track-"]');
        const temp = Array.from(elems, elem => elem.value);
        elems.forEach(elem => {

          // Initialize the previous selection.
          elem.value && (elem.dataset.prev = elem.value);
          Array.from(elem.options).forEach(opt => {
            opt.disabled = opt.value
                && opt.value != elem.value
                && temp.includes(opt.value);
          });

          // Register event listeners.
          elem.addEventListener('change', evt => {
            // Enable and disable based on the selection made.
            const val = evt.target.value
            const prev = evt.target.dataset.prev;
            elems.forEach(sel => {
              if (sel != evt.target) {
                Array.from(sel.options).forEach(opt => {
                  if (opt.value == val && val != '') {
                    opt.disabled = true;
                  } else if (opt.value == prev) {
                    opt.disabled = false;
                  }
                });
              }
            });
            evt.target.dataset.prev = val;
          });
        });

      })()
    </script>
  </body>
</html>
  • Related