Re:ゼロから始める文系プログラマ

未経験がプログラミングを通して人生を変える

【Flask】電子掲示板を作る~⑤投稿作成ページを機能させる~


スポンサードリンク
 

f:id:ShotaNukumizu_1000:20210718062603p:plain

おはようございます。Shotaです。

今日は一昨日に引き続き、PythonのFlaskを用いた電子掲示板の作り方について解説していきます。

今日の記事では投稿を作るページを機能させる方法を徹底解説します。



投稿作成ページを機能させる

一昨日の記事では、Pythonファイル「main.py」とhtmlファイル「create.html」を下の部分まで仕上げました。

▼main.py

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///postdata.db'
db = SQLAlchemy(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user = db.Column(db.String(20), nullable=False)
    title = db.Column(db.String(30), nullable=False)
    detail = db.Column(db.Text, nullable=False)
    post_date = db.Column(db.DateTime, nullable=False)

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

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

if __name__ == '__main__':
    app.run(debug=True)

▼create.html

{% extends 'base.html' %}

{% block body %}
<h1>新規作成</h1>

<div class="container">
    <form action="/" method="POST">
        <p>日付:<input type="date" name="post_date"></p>
        <input class="form-control" name="user" type="text" placeholder="名前(全角20文字以内)" aria-label="default input example">
        <input class="form-control" name="title" type="text" placeholder="タイトル(全角30文字以内)" aria-label="default input example">
        <div class="mb-3">
            <label for="exampleFormControlTextarea1" class="form-label">投稿内容</label>
            <textarea class="form-control"  name="detail" id="exampleFormControlTextarea1" rows="3"></textarea>
        </div>
        <button href="/" class="btn btn-outline-success">戻る</button>
        <button type="submit" class="btn btn-success">投稿する</button>
    </form> 
</div>

{% endblock %}

Pythonファイルを実行してサーバを立ち上げ、/createページにアクセスすると以下のように表示させることができます。

f:id:ShotaNukumizu_1000:20210824064831p:plain

しかし、上記のファイルのままでは投稿作成ページを機能させることはできません。(データを入力して「投稿する」ボタンを押すと、下のようなエラーが表示されます)

f:id:ShotaNukumizu_1000:20210824064748p:plain

フォームのアクセス先の設定が原因でこのような結果になります。

というのも、フォーム送信の宛先はaction="/"でトップページになっていますが、そのトップページではPOSTを受けられる状態になっていません。

言い換えれば、GETしか受け取れない状態になっています。

そのためには、Pythonファイルを編集して機能させる必要があります。

そこで、トップページにルーティングしている関数のデコレータ部分を、少し変更してあげましょう。

@app.route('/') #このコードを
@app.route('/', methods=['GET', 'POST']) #このように変更する


上記のように書き換えることで、トップページでPOSTメソッドを受け入れる準備ができました。なので、もう一度/createにアクセスして、投稿を入力して送信してみます。

そうすると、元の画面に戻ってこれるはずです。

少し現状を整理してみましょう。

  • /createページにアクセスして、投稿内容を作成することができた
  • タスク作成後は、トップページに遷移できるようになった
  • しかし、入力したタスクを保存、および表示できていない


したがって、あとはトップページのコードを変更してあげて、

①データベースにタスクを保存
②保存されているタスクを表示する


以上の機能を実装すると良さそうですね。


投稿内容の保存・表示


Pythonファイルの操作

①データベースにタスクを保存
②保存されているタスクを表示する


これらをトップページで実装していきます。そのためには、以下のコードをいじっていきます。

@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')


追加のインポート

まずは、追加で以下のコードをimportしましょう。

追加するのはrequestredirectの2つのクラスです。

from flask import Flask, render_template #これだったけど
from flask import Flask, render_template, request, redirect #こちらに変更


以下の挙動をプログラムに付け加えていこうと思います。

  • POSTメソッド:データベースにタスクを保存する
  • GETメソッド:保存されているタスクを表示


このように、リクエスト方法に応じて実装内容を変更していく際にrequestが必要になります。

またredirectは、POSTで受け取った内容をデータベースに反映した後に、再度トップページにアクセスするために使います。

投稿内容の保存と表示をするコードを書いていきましょう。以下のようにmain.pyを編集してあげます。

from datetime import datetime

# 中略

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        posts = Post.query.all()
        return render_template('index.html', posts=posts)
    else:
        user = request.form.get('user')
        title = request.form.get('title')
        detail = request.form.get('detail')
        post_date = request.form.get('post_date')

        post_date = datetime.strptime(post_date, '%Y-%m-%d')
        new_post = Post(user=user, title=title, detail=detail, post_date=post_date)

        db.session.add(new_post)
        db.session.commit()
        return redirect('/')


GETとPOSTに分解して考えます。


GET―タスクの表示部分

if request.method == 'GET':
     posts = Post.query.all()
     return render_template('index.html', posts=posts)

この部分でやっていることは、「データベースからすべての投稿を取り出し、それをトップページに渡す」ことです。


POST―タスクの保存部分

else:
      user = request.form.get('user')
      title = request.form.get('title')
      detail = request.form.get('detail')
      post_date = request.form.get('post_date')

      post_date = datetime.strptime(post_date, '%Y-%m-%d')
      new_post = Post(user=user, title=title, detail=detail, post_date=post_date)

      db.session.add(new_post)
      db.session.commit()
      return redirect('/')

もしリクエストがPOSTなら、データベースに投稿を保存します。

上記でやっていることは主に3つです。

  • POSTされた内容を受け取る
  • Postクラスに受け取った内容を渡す
  • データベースに投稿を保存する


上述がデータベースへ投稿するまでのステップです。

データベースへ保存するときは、まずaddで内容を追加して、実際に反映するためにcommitします。

post_date = datetime.strptime(post_date, '%Y-%m-%d')


また、途中で変数post_dateをキャスト(Pythonにおける、データの型変換)している部分があります。これは、データベースの定義でフォームから受け取る値が文字列だからです。よって、Python上で文字列から日付型にキャストしています。

これでPython側でやることは完了しましたので、あとは投稿内容を反映するためにhtmlを編集するだけです。


HTMLファイルの操作

Bootstrapにアクセスして、index.htmlを編集します。

getbootstrap.jp

「card」と検索して、以下のカードを使います。

f:id:ShotaNukumizu_1000:20210825080722p:plain

そして、次のコードをindex.htmlにコピペしてください。

<div class="card">
  <h5 class="card-header">Featured</h5>
  <div class="card-body">
    <h5 class="card-title">Special title treatment</h5>
    <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
    <a href="#" class="btn btn-primary">Go somewhere</a>
  </div>
</div>

index.htmlの全体像は次の通りです。

{% extends 'base.html' %}

{% block body %}
<h1>Index Page</h1>

<div class="card">
    <h5 class="card-header">Featured</h5>
    <div class="card-body">
      <h5 class="card-title">Special title treatment</h5>
      <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
      <a href="#" class="btn btn-primary">Go somewhere</a>
    </div>
</div>
{% endblock %}

これでPythonファイルを実行してサーバを立ち上げると、次のように画面が表示されるかと思います。

f:id:ShotaNukumizu_1000:20210825081107p:plain

ただし、これだと見栄えが良いとは言えないのでcardクラスが付いてあるdivタグの上に次のコードを載せてください。

<div class="container"></div>

完成形は次のようになります。

<div class="container">
  <div class="card">
      <h5 class="card-header">Featured</h5>
      <div class="card-body">
        <h5 class="card-title">Special title treatment</h5>
        <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
  </div>
</div>

これで再度サーバを立ち上げてみてください。

f:id:ShotaNukumizu_1000:20210825081540p:plain

このように形が整うと思います。あとは、次のような機能をindex.html上に実装していきます。

  • 「投稿する」ボタンを作る(同時にページに遷移させる)
  • カードに日付と投稿した日時、タイトルを記す
  • 「詳細」「削除」ボタンを付ける


まずは、投稿するボタンを作りましょう。再度Bootstrapにアクセスして、「button」と検索します。ボタンは次のようなものを使っていきます。アレンジ等はご自由にやってもらって大丈夫です。

▼使うボタンのソース

f:id:ShotaNukumizu_1000:20210825082228p:plain

「投稿する」ボタンのコードは、次のようなものを使います。

<button type="button" class="btn btn-info">Info</button>

これを、投稿ページに遷移できるように以下のようにアレンジします。

<button href="/create" type="button" class="btn-lg btn-info">投稿する</button>

これをindex.htmlにコピペしてください。index.htmlの完成形は次のようになります。

{% extends 'base.html' %}

{% block body %}
<h1>Index Page</h1>

<button href="/create"  class="btn-lg btn-info">投稿する</button>

<div class="container">
  <div class="card">
      <h5 class="card-header">Featured</h5>
      <div class="card-body">
        <h5 class="card-title">Special title treatment</h5>
        <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
  </div>
</div>
{% endblock %}

コードが整ったら、Pythonファイルを実行してindex.htmlを確認してみましょう。そうすると、以下のような画面が表示されると思います。

f:id:ShotaNukumizu_1000:20210825083052p:plain

今度は「詳細」ボタンと「削除」ボタンを付けていきます。以下のコードをコピペしてください。

▼「詳細」ボタン

<button type="button" class="btn btn-secondary">Secondary</button>

▼「削除」ボタン

<button type="button" class="btn btn-danger">Danger</button>

これらをアレンジして、index.htmlに加えると、次のようになります。

{% extends 'base.html' %}

{% block body %}
<h1>Index Page</h1>

<button href="/create" class="btn-lg btn-info">投稿する</button>

<div class="container">
  <div class="card">
      <h5 class="card-header">Featured</h5>
      <div class="card-body">
        <h5 class="card-title">Special title treatment</h5>
        <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
        <button type="button" href="/detail" class="btn-lg btn-secondary">詳細</button>
        <button type="button" href="/delete" class="btn-lg btn-danger">削除</button>
      </div>
  </div>
</div>
{% endblock %}

これでPythonファイルを実行してサーバを立ち上げてください。そうなると、次のような画面が表示されます。

f:id:ShotaNukumizu_1000:20210825083809p:plain

あとは、カードに投稿する内容を表示するだけです。

index.htmlを次のように編集しましょう。

{% extends 'base.html' %}

{% block body %}
<h1>Index Page</h1>

<a href="/create" class="btn-lg btn-info" role="button">投稿する</a>

<div class="container">
  {% for post in posts %}
  <div class="card">
      <h5 class="card-header">日付:{{ post.post_date.date() }}</h5>
      <div class="card-body">
        <h5 class="card-title">タイトル:{{ post.title }}</h5>
        <p class="card-text">投稿者:{{ post.user }}</p>
        <a role="button" href="/detail" class="btn-lg btn-secondary">詳細</a>
        <a role="button" href="/delete" class="btn-lg btn-danger">削除</a>
      </div>
  </div>
  {% endfor %}
</div>
{% endblock %}

このように書くことで、投稿した内容を表示できます。

{% for post in posts %}{% endfor %}で、先程Pythonから渡しておいた投稿たちをfor loopしています。

さらに、ループしたpostに入っているタイトルをpost.title、投稿者をpost.user、日付をpost.post_date.date()のように書けば中身を取り出せます。

なお、表示したい変数部分は{}を二重重ねで囲むのが原則です。

これで、サーバを立ち上げて/createにアクセスして投稿を作成して「投稿する」ボタンを押してみます。

f:id:ShotaNukumizu_1000:20210825085751p:plain

これで、トップページに遷移しつつ投稿内容を表示されるようになりました。

さらに、投稿を追加すれば2つも表示されるようになります。

f:id:ShotaNukumizu_1000:20210825085952p:plain

これで十分アプリケーションに近づいたと思います。

投稿の作成に加えて、あとは次のような実装を加えていきます。

  • 投稿内容を確認する
  • 投稿を削除する


これ以上説明すると記事が長くなってしまいますので、今回の記事はここまでにしておきます。


まとめ

今日の記事では主に次のことについて解説しました。

今日使ったファイルとそのソースコードを次に示しておきます。

main.py

from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, date

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///postdata.db'
db = SQLAlchemy(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user = db.Column(db.String(20), nullable=False)
    title = db.Column(db.String(30), nullable=False)
    detail = db.Column(db.Text, nullable=False)
    post_date = db.Column(db.DateTime, nullable=False)

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        posts = Post.query.all()
        return render_template('index.html', posts=posts)
    else:
        user = request.form.get('user')
        title = request.form.get('title')
        detail = request.form.get('detail')
        post_date = request.form.get('post_date')

        post_date = datetime.strptime(post_date, '%Y-%m-%d')
        new_post = Post(user=user, title=title, detail=detail, post_date=post_date)

        db.session.add(new_post)
        db.session.commit()
        return redirect('/')


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

if __name__ == '__main__':
    app.run(debug=True)

index.html

{% extends 'base.html' %}

{% block body %}
<h1>Index Page</h1>

<a href="/create" class="btn-lg btn-info" role="button">投稿する</a>

<div class="container">
  {% for post in posts %}
  <div class="card">
      <h5 class="card-header">日付:{{ post.post_date.date() }}</h5>
      <div class="card-body">
        <h5 class="card-title">タイトル:{{ post.title }}</h5>
        <p class="card-text">投稿者:{{ post.user }}</p>
        <a role="button" href="/detail" class="btn-lg btn-secondary">詳細</a>
        <a role="button" href="/delete" class="btn-lg btn-danger">削除</a>
      </div>
  </div>
  {% endfor %}
</div>
{% endblock %}

create.html

{% extends 'base.html' %}

{% block body %}
<h1>新規作成</h1>

<div class="container">
    <form action="/" method="POST">
        <p>日付:<input type="date" name="post_date"></p>
        <input class="form-control" name="user" type="text" placeholder="名前(全角20文字以内)" aria-label="default input example">
        <input class="form-control" name="title" type="text" placeholder="タイトル(全角30文字以内)" aria-label="default input example">
        <div class="mb-3">
            <label for="exampleFormControlTextarea1" class="form-label">投稿内容</label>
            <textarea class="form-control"  name="detail" id="exampleFormControlTextarea1" rows="3"></textarea>
        </div>
        <a href="/" class="btn btn-outline-success">戻る</a>
        <button type="submit" class="btn btn-success">投稿する</button>
    </form> 
</div>

{% endblock %}

明日の記事では、投稿内容の詳細を表示するプログラムを書いていきます。

長くなりましたが、今日の記事はこれで終了です。


開発環境

【参考サイト】

tech-diary.net