Flask 使用 SelectField 为列表中的每个项目创建表单

Flask create form with SelectField for every item in a list

我是 Flask 的新手,我正在尝试做一些需要一些技能的东西,我在搜索 SO 时找不到答案。

所以,概念是...我有一个动态生成的歌曲列表。基本上我们不知道会有多少。歌曲存储在列表中的列表中,如下所示:

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

我想为列表中的每个列表创建一个带有 SelectField 的表单,以便用户可以为列表中的每首歌曲打分。

传递列表项的路由如下所示:

@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)

接收列表的路由如下所示:

@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)

我无法弄清楚 SongsRated 的形式应该是什么 return SelectFields 的动态数量。我还应该能够收集 return 值并确定哪个 SelectField 值属于列表中的哪个项目(歌曲)。

最后我想做一些验证,因为我只希望用户能够对 10 首歌曲(无论多少)进行评分,并且分数应该都是唯一的 (1-10)。

如果没有解释清楚,我很抱歉。

提前致谢。

您可以在 this 教程中找到有关如何在视图中动态创建表单的说明。
基于此,我给你写了以下示例。
为列表中的每个项目创建一个带有 select 字段的表单。如果提交了表单,它将为您提供所有指定排名的条目的 ID 和 selected 值。标识基于字段名称,因为它包含条目的原始 ID。
双 select 离子被 custom validator.
阻止 此外,JavaScript 可以防止双 selection 发生。为此,每个 SelectField 都添加了 change-events 的侦听器,这会禁用所有其他 selected 排名,或者如果 selected 排名不同,则再次启用它。

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

LIMIT = 10 # <- HERE!!!

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

def validate_rating(form, field):
    if field.data:
        # Check if a ranking was selected twice.
        for _f in form:
            if _f != field and _f.data == field.data:
                raise ValidationError('A rating can only be given once.')
    else:
        # Check whether the number of ratings corresponds to the number of songs 
        # or the maximum limit (10).
        count,length = 0,0
        for _f in form:
            if _f.name.startswith('track'):
                length += 1
                count += int(not (_f.data == '' or _f.data == 0))
        limit = max([0, min([LIMIT, length])]) 
        if count != limit:
            raise ValidationError(f'You should give up to {limit} ratings.')

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.
    class F(SongsRated):
        pass
    for id,artist,track in songs:
        field = SelectField(
            f'{artist} - {track}',
            [
                NumberRange(min=1, max=LIMIT),
                validate_rating
            ],
            choices=list(range(LIMIT + 1)), 
            coerce=int
        )
        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 btn = document.querySelector('input[type="submit"]'); 
        const elems = document.querySelectorAll('select[name^="track-"]');
        const temp = Array.from(elems, elem => elem.value);

        const count = temp.filter(val => !(val == '' || val == 0)).length;
        const limit = Math.max(0, Math.min(10, elems.length)); // <- HERE!!!
        btn.disabled = count != limit;

        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 == '' || val == 0)) {
                    opt.disabled = true;
                  } else if (opt.value == prev) {
                    opt.disabled = false;
                  }
                });
              }
            });
            evt.target.dataset.prev = val;

            const cnt = Array.from(elems)
              .filter(elem => !(elem.value == '' || elem.value == 0))
              .length;
            btn.disabled = cnt != limit;

          });
        });

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