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

发布于 2025-01-11 03:39:56 字数 1299 浏览 0 评论 0原文

我是 Flask 的新手,我正在尝试制作一些需要一些技能的东西,但在搜索时我无法找到答案。

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

[[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 的形式应该是什么样子,应该返回动态数量的 SelectFields。我还应该能够收集返回值并确定哪个 SelectField 值属于列表中的哪个项目(歌曲)。

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

如果这没有得到很好的解释,我很抱歉。

提前致谢。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

神妖 2025-01-18 03:39:56

您可以在 这个教程。
基于此,我给您写了以下示例。
创建一个表单,其中包含列表中每个项目的选择字段。如果提交表单,它将为您提供所有指定排名的条目的 ID 和所选值。标识基于字段名称,因为它包含条目的原始 ID。
自定义验证器可以防止双重选择。
此外,JavaScript 完全可以防止双重选择的发生。为此,将更改事件的侦听器添加到每个 SelectField,这会禁用所有其他选定的排名,或者在选择不同的排名时再次启用它。

Flask (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>

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 (
    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>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文