Help us understand the problem. What is going on with this article?

WTForms の TextArea をファイルドロップに対応させる方法

経緯

ある案件をFlaskで実装したときのこと、
「Copy&Pasteしてね」と導いた公開鍵の登録フォーム(TextArea) に、
ファイルドロップをするエンドユーザさまがいらいしゃいました。
そんなことをする人がいるんだと思いながらも、なんとか対応できないものかと調べたところ...
StackOverflow: Load Textfile via Drag and Drop on Textarea
というのが見つかりました。

案外簡単なJavaScriptできることがわかったので、
Flaskで実装してみました。

Flask

まずは、Flaskで公開鍵の登録フォームをFORMクラスRegistFormとして定義します。
公開鍵の検証は sshpubkeys ライブラリを使っていますが、これは本題ではありません。

フォームのデータを検証したデータを受け取って表示するだけのテストアプリです。

import os
from flask import (
    Flask,
    request,
    render_template,
    redirect,
    url_for,
)

from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField, ValidationError
from wtforms.validators import InputRequired
from sshpubkeys import SSHKey, InvalidKeyError, MalformedDataError

class RegistForm(FlaskForm):
    pubkey = TextAreaField("PublicKey",validators=[InputRequired()])
    submit = SubmitField("OK")
    reset = SubmitField("RESET")

    def validate_pubkey(self, field):
        if field.data.find('\n') >1:
            raise ValidationError("Too Many Publickey")

        sshpubkey = SSHKey(field.data, strict=True)
        if sshpubkey.key_type != b"ssh-rsa":
            raise ValidationError("Invalid Key Type")

        try:
            sshpubkey.parse()
        except InvalidKeyError as err:
            raise ValidationError("Invalid PublicKey")
        except MalformedDataError as err:
            raise ValidationError("Malformed key Error")
        except NotImplementedError as err:
            raise ValidationError("Undefined Error")


app = Flask(__name__, static_folder='static')
app.config['SECRET_KEY']='12345678901234567890'

@app.route("/", endpoint="register", methods=["GET", "POST"])
def register():
    form = RegistForm()
    if request.method == "GET":
        return render_template("test.html",
                               form=form,
                               endpoint="register")

    if form.validate_on_submit():
        return form.pubkey.data
    else:
        print(f'vaslidation error')
        return redirect(url_for("register"))


if __name__ == "__main__":
    app.run(
        host=os.getenv("APP_HOST", "localhost"),
        port=os.getenv("APP_PORT", 8080),
        debug=True,
    )

JavaScript

static/functions.js に次の関数を用意します。

function dropfile(file) {
    var reader = new FileReader();
    reader.onload = function(e) {
        notepad.value = e.target.result.replace(/(?:\r\n|\r|\n)/g, '');
    };
    reader.readAsText(file, "UTF-8");
};

notepad.ondrop = function(e) {
    e.preventDefault();
    var file = e.dataTransfer.files[0];
    dropfile(file);
};

DataTransferオブジェクトを使っていますが、すべてのブラウザがオブジェクトを公開するわけではありません。モダンブラウザであれば問題ないはずですが...

HTML

実は、コツらしいものといえばここぐらいなものです。
forms.py で定義したクラスのフィールドにアトリビュート id に "notepad" を与えています。

{% extends "base.html" %}

{% block contents %}

<form action="{{ url_for( endpoint ) }}" method="post">
{{ form.hidden_tag() }}
{{ form.pubkey(id="notepad", raws=6, cols=80,
               placeholder="Copy&Past or Drag&Drop here!") }}
{{ form.submit }}
{{ form.reset }}
</form>
{% endblock contents %}

rowscolsplaceholder はクラス定義のなかで render_kw に辞書形式で与えることもできますが、描画に関わるものをあまりクラスで定義しない方がきれいだと思っています。

render_kw={"rows": 6, "cols": 80, "placeholder": "Copy&Paste" }

これも本題とはあまり関係ないものですが、気になる方もおられるでしょうから、
とりあえずこんな base.html を用意します。

{% from "_defaults.html" import render_htmlattribs, render_styles, render_scripts %}

{% block doc -%}
<!DOCTYPE html>
<html{% block html_attribs %} {{ render_htmlattribs() }}{% endblock html_attribs %}>
  {%- block html %}
  <head>
    {%- block head %}
    <title>{% block title %}{{ render_title() }}{% endblock title %}</title>

      {%- block metas %}
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta charset="UTF-8">
      {%- endblock metas %}
      {%- block favicon %} {%- endblock favicon %}

      {%- block styles %}
      {{ render_styles() }}
      {%- endblock styles %}

    {%- endblock head %}
  </head>
  <body{% block body_attribs %}{% endblock body_attribs %}>
    {%- block body %}

    {% block contents %} {% endblock contents %}

    {% block footer %}
      {{ render_footer() }}
    {%- endblock footer %}

    {% block scripts %}
      {{ render_scripts() }}
    {%- endblock scripts %}

    {%- endblock body %}
  </body>
  {%- endblock html %}
</html>
{% endblock doc -%}

_defaults.html で定義したマクロは次のようなものです。
使いまわしているので bootstrap とか jquery とか読み込むようにしていますが、
これがなくても構いません。
static/functions.js さえ読み込んでいればOKです。

{% macro render_htmlattribs() -%}
lang="ja"
{%- endmacro %}

{% macro render_styles() %}
    <link href="/static/bootstrap-4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/base.css" rel="stylesheet">
{% endmacro %}


{% macro render_scripts() %}
    <script src="/static/jquery-3.5.1.min.js"></script>
    <script src="/static/bootstrap-4.5.2/js/bootstrap.min.js"></script>
    <script src="/static/functions.js"></script>
{% endmacro %}

参考資料

* <input type="file" /> でファイル選択させたいときはこちらが参考になります。
  アップロードしたファイルをテキストで読み込む

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away