掲示板を作る(Flaskを使ってYoutubeやXのような自作のwebアプリを作ってみよう)

はじめに

python用の軽量webアプリケーションフレームワークであるFlaskを使ってYoutubeやX(旧Twitter)などwebアプリの基本的な動作原理を学び、実際に自分で作れるようになりましょう!私が制作したアプリを基に解説しています。

今回は登録者同士で交流ができる掲示板を解説していきたいと思います。前回の登録フォームでPOSTを使ったデータベースの操作も使いますので、前回の記事も参考にしてください。今回はデータベースから取得したデータを掲示板上に表示させる方法や削除する方法を解説します。これら方法に苦労しましたので、少し工夫があります。そのあたりを詳しく解説できたらと思います。

過去の記事

解説

掲示板の完成図

まずは掲示板の完成図がこちらになります。

掲示板というタイトルがあり、その下に書き込むスペースがあります。書き込んだ後に書き込むボタンを押すことで下に表示するという流れになります。また、書き込まれたものを削除できるボタンも設置します。

上の掲示板を作るために4つのポイントを抑える必要があります。

①掲示板を書き込むスペースを作成する。

②書き込むボタンを押したあと、書き込んだユーザと書き込んだ内容と書き込んだ時間をデータベースに保存する

③データベースから書き込まれた内容を表示する

④削除ボタンを押されたらデータベースからその内容のみを削除できる

これらをコードに反映させていきます。

コードの解説

全体像

掲示板のhtmlです。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>一人生活の部屋</title>
  <meta name="description" content="独身一人暮らしの生活に役立つ情報を配信。趣味・お金・料理・仕事">
  <!-- リセットCSS -->
  <link rel="stylesheet" href="https://unpkg.com/ress@4.0.0/dist/ress.min.css">
  <link rel="stylesheet" href="{{ url_for('static', filename='css/common-board/style.css') }}">
  <link rel="stylesheet" href="{{ url_for('static', filename='css/common-board/pre-min.css') }}">

</head>

<body>
  <h1><a id="menu-switch" href="/">≡</a>
    掲示板</h1>
  <!-- ①掲示板に書き込むところ(ここから) -->
  <form action="/write" method="POST" class="pure-form pure-form-stacked">   <!-- POSTでpythonに渡す -->
    <textarea class="write-area" name="ta" rows="4" cols="60"></textarea>    <!-- 書き込んだ内容をtaという名前でpythonに渡す -->
    <button type="submit" class="button">
      書き込む</button>
  </form>
 <!-- ①掲示板に書き込むところ(ここまで) -->
  <!-- ③書き込まれた内容を表示するところ(ここから) -->
  {% for i in data %}         <!-- データベースからfor文ですべて表示させる -->
  {% if i.text |length == 0 %}    <!-- 予期せぬテキストが表示されてしまうのでifで分岐させる -->
  <p class="none-text"></p>       <!-- 予期せぬテキストを非表示にさせる -->
  {% else %}
  <div class="box">
    <form action="/remove" method="POST" class="pure-form">   <!-- POSTでpythonに渡す -->
      <p class="box_h">名前 {{ i.name }}     -     投稿時間 {{ i.date }}</p>     <!-- 投稿したユーザと投稿時間を表示する -->
      <textarea class="none-text" name="remove_name" cols="20" rows="1" maxlength="100" readonly>{{ i.name }}</textarea>   <!-- 削除するときにユーザ名を取得するのに必要、表示はされない -->
      <textarea class="text-auto-height" name="te" cols="20" rows="2" maxlength="100" readonly>{{ i.text }}</textarea>     <!-- 投稿した内容を表示する -->
      <div align="right"><button type="submit" class="button{{ loop.index0 }} button">
        削除</button></div>
    </form>
  </div>
  {% endif %}
  {% endfor %}
 <!-- ③書き込まれた内容を表示するところ(ここまで) -->
  </div>

</body>

</html>

この掲示板は大きく分けて2か所に分かれています。
・掲示板に書き込むところ
・書き込まれた内容を表示するところ
です。

掲示板に書き込むところ

まずは掲示板に書き込むところは以下のようになります。

  <form action="/write" method="POST" class="pure-form pure-form-stacked">   <!-- POSTでpythonに渡す -->
    <textarea class="write-area" name="ta" rows="4" cols="60"></textarea>    <!-- 書き込んだ内容をtaという名前でpythonに渡す -->
    <button type="submit" class="button">
      書き込む</button>
  </form>

書き込むボタンを押したらPOSTでpythonに渡します。
その際、書き込みスペースの文字はtaという名前でpythonに渡します。

python側では以下のようなコードで受け取り、データベースを操作します

@app.route('/common_board')
def common_board():
	return render_template('front-room/common-board/common-board.html', data=data2.load_data())

@app.route('/write', methods=['POST'])
def write():                         #書き込みボタンが押されたら実行される
    if not user.is_login():                 #ログインされているユーザかどうか確認
        return redirect('/room_login')
    ta = request.form.get('ta', '')                      #書き込まれた内容を取得する
    if ta == '': return msg('書込が空でした。')           #書き込まれた内容が何もなかったら「書込が空でした」と表示
    data2.save_data_append(                 #書き込まれたユーザと内容をデータベースに追加 
            user=user.get_id(),
            text=ta)
    return redirect('/common_board')

4行目のrequest.form.getで書き込まれた内容(ta)を受け取ります。もし、taが空白なら「書込が空でした」と表示されるようにします。内容があればユーザ名と内容をデータベースに追加します。データベース構造は以下のように作成しています。

board_dataというデータベース名にname、text、dateというキーを作成し、ユーザ名、書き込まれた内容、日付を格納します。

データベースを操作するコードは以下のようになります。こちらはtinydbというライブラリを使っています。詳細はこちらを参照してください。

import os, json, datetime
from tinydb import TinyDB, Query

BASE_DIR = os.path.dirname(__file__)
SAVE_FILE = BASE_DIR + '/board-data/board_data.json'

db = TinyDB(SAVE_FILE)

def get_text_table():
    return db.table('board_data'), Query()

def load_data():
    table, q = get_text_table()
    return table.all()

def get_text(id):
    table, q = get_text_table()
    return table.search(q.id == id)

def save_data_append(user, text):
    table, q = get_text_table()
    table.insert({
        'name': user,
        'text': text,
        'date': get_datetime_now()})

def remove_text(id, text):
    table, q = get_text_table() 
    return table.remove((q.name == id) and (q.text == text)) 

def get_datetime_now():
    now = datetime.datetime.now()
    return "{0:%Y/%m/%d %H:%M}".format(now)

書き込まれた内容を表示・削除する

掲示板に表示する

次に書き込まれた内容を表示するところです。

  {% for i in data %}         <!-- データベースからfor文ですべて表示させる -->
  {% if i.text |length == 0 %}    <!-- 予期せぬテキストが表示されてしまうのでifで分岐させる -->
  <p class="none-text"></p>       <!-- 予期せぬテキストを非表示にさせる -->
  {% else %}
  <div class="box">
    <form action="/remove" method="POST" class="pure-form">   <!-- POSTでpythonに渡す -->
      <p class="box_h">名前 {{ i.name }}     -     投稿時間 {{ i.date }}</p>     <!-- 投稿したユーザと投稿時間を表示する -->
      <textarea class="none-text" name="remove_name" cols="20" rows="1" maxlength="100" readonly>{{ i.name }}</textarea>   <!-- 削除するときにユーザ名を取得するのに必要、表示はされない -->
      <textarea class="text-auto-height" name="te" cols="20" rows="2" maxlength="100" readonly>{{ i.text }}</textarea>     <!-- 投稿した内容を表示する -->
      <div align="right"><button type="submit" class="button{{ loop.index0 }} button">
        削除</button></div>
    </form>
  </div>
  {% endif %}
  {% endfor %}

この掲示板が開かれたときにdataという変数がpythonから渡されます。このdataというのはデータベースから取得されたデータになっています。

htmlが受け取ったdataをループで格納されているユーザ名、内容、日付を1つずつ取り出します。ユーザ名、内容、日付の1組を i として格納しています。

そしてこの i からname(ユーザ名)やtext(内容)やdate(日付)を取り出し、表示させています。取り出し方は i の後に「.キー」でできます。

予期しないユーザ名が表示されることがありましたので、はじめの処理として、i.textが0文字ならnone-textというclassで非表示にさせています。

あとは最終的な表示を目指して作成します。作成しますが、削除できるように工夫もしていきます。

表示させた内容とユーザ名をリンクさせないと同じ内容を書いたユーザも消えてしまいますので、書いた内容とユーザ名をリンクさせます。リンクさせるためには表示している内容とユーザ名をpythonに渡さなければなりません。

そこで、削除ボタンが押されたときにremove_nameとteというnameタグでpythonに渡します。それぞれをnameがデータベースにあった時に削除できるようにしています。

ただ、remove_nameをhtmlで書いてしまうと掲示板にも表示されてしまいますので、こちらのclassにnone-textというcssで非表示させています。これで、掲示板がキレイになります。

掲示板から削除する

ではremove_nameとteを削除するpythonを以下に示します。

htmlで削除ボタンがおされたらPOSTでpythonに渡されます。

app.route('/remove', methods=['POST'])
def remove():                         #削除ボタンが押されたら実行
    if not user.is_login():                  #ログインされているユーザかどうか確認
        return redirect('/room_login')
    te = request.form.get('te')               #削除する内容を取得する
    remove_user_name = request.form.get('remove_name')  #削除するユーザを取得する
    if user.get_id() == remove_user_name:
        data2.remove_text(id=user.get_id(), text=te)   #削除するユーザと内容をデータベースから削除する
        return redirect('/common_board')
    else:
        return redirect('/common_board')

まずはログインされているか確認をします。ログインされていたらremove_nameとteをhtmlから受け取ります。remove_nameとログインしているユーザ名が一致していたらteをデータベースから削除します。これで掲示板から内容が削除されます。

最後に

今回は掲示板の解説をしました。掲示板では
・掲示板に書き込むところ
・書き込まれた内容を表示する
・書き込んだ内容を削除する
ことをコーディングしました。
webアプリ作成での醍醐味となっておきました。いろいろな知識が徐々に活躍してくるかと思います。頑張って勉強していきましょう。

これまで解説したコードの全体像

from flask import Flask, render_template, redirect, url_for, session
from flask import request, Markup, flash
import os, time, locale
import secrets

locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')

from utils import my_url_for
import alone_space_user as user
import alone_space_room_data as data
import alone_space_board_data as data2
import alone_space_registar_user as user_data

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

readindex = open('index.txt', 'r', encoding="utf-8")
@app.route('/front_room')
def front_room():
    readindexlist = readindex.readlines()
    indexlistname = []
    for num in range(len(readindexlist)):
        indexlistname.append('header-text' + str(num))
    return render_template('front_room.html', readindexlist=readindexlist, indexlistname=indexlistname)

@app.route('/register', methods=['GET', 'POST']) 
def register():
    if request.method == "POST":
        user_name = request.form["user_name"]
        message = request.form["message"]

        is_valid = True
        if not user_name:
            flash("ユーザー名を入力してください。")
            is_valid = False

    if not message:
            flash("パスワードを入力してください。")
            is_valid = False

        if user_data.get_user_list(user_name):
            flash("ユーザーが存在します。違う名前を入力してください。")
            is_valid = False

        if len(message) <= 10:
            flash("パスワードは11文字以上を入力してください")
            is_valid = False

        if is_valid:
            user_data.add_user(user_name, message)
            return redirect('register_result')
   
    return render_template('front-room/first-visitor/register.html')

@app.route('/register_result')
def register_result():
    return render_template('front-room/first-visitor/register-result.html')

@app.route('/common_board')
def common_board():
	return render_template('front-room/common-board/common-board.html', data=data2.load_data())

@app.route('/write', methods=['POST'])
def write():
    if not user.is_login():
        return redirect('/room_login')
    ta = request.form.get('ta', '')
    if ta == '': return msg('書込が空でした。')
    data2.save_data_append(
            user=user.get_id(),
            text=ta)
    return redirect('/common_board')

@app.route('/remove', methods=['POST'])
def remove():
    if not user.is_login():
        return redirect('/room_login')
    #elif user.is_login() == user.get_id():
    te = request.form.get('te')
    remove_user_name = request.form.get('remove_name')
    print(user.get_id)
    print(remove_user_name)
    if user.get_id() == remove_user_name:
        data2.remove_text(id=user.get_id(), text=te)
        return redirect('/common_board')
    else:
        return redirect('/common_board')
{% extends "layout.html" %}

{% block content %}

<body>
  <h1 class="homepage-name">Al<span>(l)</span>-one Space</h1>


  <div class="wall">
    <div class="header-text">
      <p>趣味や好きなことに多忙な人向けの<br>
        <span>To Do List</span><br>
        ちょっぴり自由度の高いwebサイト<br>
        そして<span>たまに</span>共有</p>
    </div>

    <div class="enter">
      <h2 class="enter-text">3つの部屋へ(クリック↓)</h2>
      <div class="door">
        <a href="{{ url_for ('front_room' )}}">
          <img src="{{ url_for('static', filename='images/header/door_close.png') }}" alt="door-close">
        </a>
      </div>
    </div>
  </div>
  </div>


  {% endblock%}
{% extends "layout.html" %}

{% block content %}
  <div class="wall-front-room">
    <!-- <h1 class="front-room">Alone Spaceへようこそ</h1> -->
    <h2 class="front-test">お好きな部屋へどうぞ</h2>
    <div class="door0 flex">
      <div class="door1 front-door">
        <h2 class="door-text door1-text">初めての方<br>~当サイトの説明~</h2>
        <div class="door1-img"><a href="{{ url_for ('first_visitor' )}}"><img
              src="{{ url_for('static', filename='/images/header/door_close.png') }}" alt="door-close"></a></div>
      </div>
      <div class="door2 front-door">
        <h2 class="door-text door2-text">自分の部屋へ<br>(ログインが必要です)</h2>
        <div class="door2-img"><a href="{{ url_for ('room' )}}"><img
              src="{{ url_for('static', filename='/images/header/door_close.png') }}" alt="door-close"></a></div>
      </div>
      <div class="door3 front-door">
        <h2 class="door-text door3-text">共通掲示板</h2>
        <div class="door3"><a href="{{ url_for ('common_board' )}}"><img
              src="{{ url_for('static', filename='/images/header/door_close.png') }}" alt="door-close"></a></div>
      </div>
    </div>
  </div>

  <div class="information">
    <h2 class="information-text">~~~お知らせ~~~</h2>
    <div>
      <ul class="information-list">
        {% for indextext in readindexlist %}
        <li class="{{ indexlistname[loop.index0] }} information-text">{{ indextext }}</li>
        {% endfor %}
      </ul>
    </div>
  </div>
  {% endblock%}
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Alone Space</title>
  <meta name="description" content="趣味や好きなことに多忙な人向けのTo Do List。ちょっぴり自由度の高いwebサイトそしてたまに共有">
  <link
    href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&family=Poppins:ital,wght@0,800;1,700&display=swap"
    rel="stylesheet">

  <!-- リセットCSS -->
  <link rel="stylesheet" href="https://unpkg.com/ress@4.0.0/dist/ress.min.css">
  <link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='css/style.css') }}">

</head>

<body>

  {% block content %}{% endblock%}

</body>

</html>
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>一人生活の部屋</title>
  <meta name="description" content="独身一人暮らしの生活に役立つ情報を配信。趣味・お金・料理・仕事">
  <!-- Google Fonts -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link
    href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&family=Poppins:ital,wght@0,800;1,700&display=swap"
    rel="stylesheet">

  <!-- リセットCSS -->
  <link rel="stylesheet" href="https://unpkg.com/ress@4.0.0/dist/ress.min.css">

  <link rel="stylesheet" href="{{ url_for('static', filename='css/first-visitor/first-visitor.css') }}">

</head>

<body>

  <main>
    <h1 class="first-visitor-title">初めての方</h1>
    <h2 class="welcome">ようこそAl<span>(l)</span>-one Spaceへ</h2>

    <div class="first-visitor-text box">
      <p class="box">当初、本サイトは単身者向けの情報共有サービスとして作成しようと思いました。</p>
      <p class="box">情報共有だけでなく、生活や趣味などのやることリストをまとめるサイトがあったらなと思うようになり、<span>「web上の自分だけの部屋」</span>という名目で本サイトを作成しました。</p>
      <p class="box">百聞は一見に如かずということで管理者の部屋をご紹介させていただきます。</p>
      <a class="box-a" href="{{ url_for('example_room' )}}">例 : 管理者の部屋</a>
      <p class="box">また、部屋に機能を追加したいや何か要望などありましたら、部屋下部の開発依頼で申請してもらえれば開発者の能力次第ですが追加していきます。</p>
      <a class="box-a" href="https://all-one-life.com/">開発者の能力など紹介(ブログ)</a>
      <p class="box">本サイトは部屋だけでなく、共有の掲示板もあります。書き込む場合は登録が必要ですが、読むだけなら登録せずに入ることができます。</p>
      <a class="box-a" href="{{ url_for('common_board' )}}">共通掲示板</a>
      <p class="box">部屋でやりたいことリストを作成し、掲示板で交流して情報収集などしてもらえればと思います。</p>
    </div>
  </main>
</body>

</html>
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>一人生活の部屋</title>
  <meta name="description" content="独身一人暮らしの生活に役立つ情報を配信。趣味・お金・料理・仕事">
  <!-- リセットCSS -->
  <link rel="stylesheet" href="https://unpkg.com/ress@4.0.0/dist/ress.min.css">
  <link rel="stylesheet" href="{{ url_for('static', filename='css/room-template/room-template.css') }}">

</head>

<body>
  <h1>登録フォーム</h1>

  {% with messages = get_flashed_messages() %}
  {% if messages %}
  <ul>
    {% for message in messages %}
    <li>{{ message }}</li>
    {% endfor %}
  </ul>
  {% endif %}
{% endwith %}

<form action="" method="POST" novalidate="novalidate">
  <p>
    <label for="user_name">名前:</label><input type="text" name="user_name" />
  </p>
  <p>
    <label for="message">パスワード</label><textarea name="message" cols="20" rows="2"></textarea>
  </p>
  <p><button type="submit" class="button">登録</button></p>
</form>

<div class="footer">
  <a id="menu-switch" href="/">トップページ</a>
</div>
</body>

</html>
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>一人生活の部屋</title>
  <meta name="description" content="独身一人暮らしの生活に役立つ情報を配信。趣味・お金・料理・仕事">
  <!-- リセットCSS -->
  <link rel="stylesheet" href="https://unpkg.com/ress@4.0.0/dist/ress.min.css">
  <link rel="stylesheet" href="{{ url_for('static', filename='css/common-board/style.css') }}">
  <link rel="stylesheet" href="{{ url_for('static', filename='css/common-board/pre-min.css') }}">

</head>

<body>
  <h1><a id="menu-switch" href="/">≡</a>
    掲示板</h1>
  <!-- ①掲示板に書き込むところ(ここから) -->
  <form action="/write" method="POST" class="pure-form pure-form-stacked">   <!-- POSTでpythonに渡す -->
    <textarea class="write-area" name="ta" rows="4" cols="60"></textarea>    <!-- 書き込んだ内容をtaという名前でpythonに渡す -->
    <button type="submit" class="button">
      書き込む</button>
  </form>
 <!-- ①掲示板に書き込むところ(ここまで) -->
  <!-- ③書き込まれた内容を表示するところ(ここから) -->
  {% for i in data %}         <!-- データベースからfor文ですべて表示させる -->
  {% if i.text |length == 0 %}    <!-- 予期せぬテキストが表示されてしまうのでifで分岐させる -->
  <p class="none-text"></p>       <!-- 予期せぬテキストを非表示にさせる -->
  {% else %}
  <div class="box">
    <form action="/remove" method="POST" class="pure-form">   <!-- POSTでpythonに渡す -->
      <p class="box_h">名前 {{ i.name }}     -     投稿時間 {{ i.date }}</p>     <!-- 投稿したユーザと投稿時間を表示する -->
      <textarea class="none-text" name="remove_name" cols="20" rows="1" maxlength="100" readonly>{{ i.name }}</textarea>   <!-- 削除するときにユーザ名を取得するのに必要、表示はされない -->
      <textarea class="text-auto-height" name="te" cols="20" rows="2" maxlength="100" readonly>{{ i.text }}</textarea>     <!-- 投稿した内容を表示する -->
      <div align="right"><button type="submit" class="button{{ loop.index0 }} button">
        削除</button></div>
    </form>
  </div>
  {% endif %}
  {% endfor %}
 <!-- ③書き込まれた内容を表示するところ(ここまで) -->
  </div>

</body>

</html>

参考

外部リンク

pythonの基礎を復習したい方

python基礎を参考にしてください。

参考文献

私が参考にしたFlaskの本

Pythonではじめる Webサービス&スマホアプリの書きかた・作りかた | クジラ飛行机 |本 | 通販 | Amazon
Amazonでクジラ飛行机のPythonではじめる Webサービス&スマホアプリの書きかた・作りかた。アマゾンならポイント還元本が多数。クジラ飛行机作品ほか、お急ぎ便対象商品は当日お届けも可能。またPythonではじめる Webサービス&ス...

コメント

タイトルとURLをコピーしました