Start_python’s diary

ふたり暮らし

アラフィフ夫婦のフリーランスプラン

FlaskでWebアプリを作成 続き1(インデックス画面と入力画面を機能させてアップデートまで)

はじめに

インデックス(見出し)画面と入力画面のレイアウトが出来上がったので、以前にログイン画面を作成したWebサイトに追加・変更していきます。

以前の内容はこちら

start-python.hateblo.jp

 

動作環境

Windows10
Python 3.7.5
Flask 1.1.1

作業フォルダ/
 ├ blog/
 │  ├ templates/
 │  │  └ blog/
 │  │      ├ base.html  ・・・(1)
 │  │      ├ edit.html  ・・・(4)
 │  │      ├ index.html  ・・・(3)
 │  │      ├ newcomoer.html
 │  │      └ top.html
 │  ├ models/
 │  │  ├ __init__.py
 │  │  ├ database.py
 │  │  ├ models.py  ・・・(2)
 │  │  └ wiki.db
 │  ├ __init__.py
 │  └ server.py    ・・・(5)
 ├ static/
 │  ├ images/
 │  │  └ sample.jpg
 │  ├ back_image.jpg
 │  ├ base.css
 │  ├ edit.css
 │  ├ edit.js
 │  ├ image.jpg
 │  ├ index.css
 │  ├ login.css
 │  ├ login.js
 │  ├ newcomer.css
 │  └ newcomer.js
 ├ templates/
 │  └ index.html
 ├ key.py
 └ main.py

 

1.ヘッダーにユーザー名を表示する

base.html

<link rel="stylesheet" type="text/css" href="/static/base.css">

<body>
    <header>

        {% if name == "" %}
        <h3 id="user_name">ようこそ ゲスト さん</h3>
        {% elif name %}
        <h3 id="user_name">{{name}}</h3>
        {% else %}
        <h3 id="user_name">ログインしてください</h3>
        {% endif %}

        <div class="menu_box">
            <label id="menu" for="toggle">メニュー</label>
            <input type="checkbox" id="toggle">
            <ul class="dropdown_menu">
                <li><a href="/blog">ホーム</a></li>
                <li><a href="/blog/create">新規作成</a></li>
                <li><a href="/blog/logout">ログアウト</a></li>
            </ul>
        </div>

    </header>


{% block body %}{% endblock %}


    <footer>
        <ul>
            <li>ホーム</li>
            <li>検 索</li>
            <li><a href="/blog/create">新規作成</a></li>
            <li><a href="/blog/logout">ログアウト</a></li>
        </ul>
        <span id="c">(c)2020 ふたり暮らし</span>
    </footer>
</body>

変更点:ヘッダーに「ユーザー名」を表示します。ユーザー名が無い場合には「ようこそ ゲスト さん」と表示します。

 

2.SQLiteでデータベースを追加・修正

models.py

from sqlalchemy import Column, Integer, String, Text, DateTime
#from models.database import Base       # データベースの初期化用
from blog.models.database import Base   # 初期化が終わればこちらへ変更
from datetime import datetime
 
 
class WikiContent(Base):
    __tablename__ = 'wikicontents'
    id = Column(Integer, primary_key=True)
    title = Column(String(128))
image_url = Column(String(128)) comment = Column(Text) date = Column(DateTime, default=datetime.now()) def __init__(self, title=None, image_url=None, comment=None, date=None): self.title = title self.image_url = image_url self.comment = comment self.date = date def __repr__(self): return '<Title %r>' % (self.title) #Userクラスを追加。WikiContentの使い回し class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) user_name = Column(String(128)) hashed_password = Column(String(128)) def __init__(self, user_name=None, hashed_password=None): self.user_name = user_name self.hashed_password = hashed_password def __repr__(self): return '<Name %r>' % (self.user_name)

追加・変更点:「id, title, image_url, comment, date」の5つのデータを扱います。

image_url・・画像のファイル名のみを保存します。(画像データは「作業フォルダ/static/images/」に保存します)

※データベースに保存する順番が「読み込む時の順番」になりますので、順番には注意が必要です。

データベースの初期化

「wiki.db」を作成します。コマンドプロンプトから下記を実行します。

cd 作業フォルダ
cd blog
python
from models.database import init_db
init_db()
quit()

modelsフォルダ内に「wiki.db」ファイルが出来ます。確認してみましょう。

※重要:初期化する時は「models.py」の2行目「#from models.database import Base」のコメント化を解除して、3行目「from blog.models.database import Base」をコメント化してください。
wiki.db」が作成されたら元に戻してください。(他にいい方法があればいいのですが、、、最初だけなので。)

 

3.インデックス(見出し)画面の修正

index.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/index.css">

<title>インデックス画面</title>

<div id="main">
    {% for diary in all_diary[::-1] %}

    <div class="table">
        <h3>サンプル1</h3>
        <h3>{{diary.title}}</h3>
        <p><img src={{diary.image_url}}></p>
        <p>{{diary.comment}}</p>
        <div class="time">{{diary.date.strftime("%Y/%m/%d %H:%M")}}</div>
    </div>

    {% endfor %}
</div>

{% endblock %}

{% for diary in all_diary[::-1] %}

Pythonでfor文を逆順で使います。(インデックスを新しい順に表示するため)

※データベースを「読み込む時の順番」に要素を配置できるように注意が必要です。

 

4.入力画面の修正

edit.html

{% extends "blog/base.html" %}
{% block body %}

<link rel="stylesheet" type="text/css" href="/static/edit.css">
<script src="/static/edit.js"></script>


<title>入力画面</title>

<form action="/blog/diarysave" method="post" enctype="multipart/form-data">

    <div id="main">
        <label>タイトル</label>
        <input id="text1" type="text" name="title">
        <br><br>
        <div id=image_box>
            <label class="btn1" for="file-sample">ファイルを選択</label>
            <input id="file-sample" type="file" name="image">
            <img id="file-preview">
        </div>
        <label>コメント</label>
        <textarea rows="12" cols="100" name="comment"></textarea>
        <div class="time"></div>
        <input class="btn1" type="submit" value="  登  録  ">
    </div>

</form>

{% endblock %}

<form action="/blog/diarysave" method="post" enctype="multipart/form-data">

入力フォームを作成します。送信ボタンをクリックすると転送処理されます「action」で送信先、「method」で転送方法、「enctype」でデータ形式、フォーム内の要素の「name」でPythonで使用する変数を設定します。

 

5.メインプログラムの追加・修正

server.py(長いので全文は後で載せます)

追加部分

#「/diarysave」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diarysave', methods=['post'])
def diarysave():  #保存
    image_url = ""
    # 画像ファイルを保存
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        image_url = filepath
    # データベースに保存
    title = request.form["title"]
    comment = request.form["comment"]
    # JSTタイムゾーンを作成
    jst = timezone(timedelta(hours=9), 'JST')
    date = datetime.now(jst)
    content = WikiContent(title,image_url,comment,date)
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("blog.index"))

画像ファイルが選択されていた場合は「static/images/」に画像データを保存します。変数「image_url」に「/static/images/画像ファイル名」を設定します。

JSTタイムゾーンを作成

「pythonanywhere」では現在時刻が「日本時間の9時間前」になってしまうので、日本の現在時刻を「date」に設定しています。

 

「server.py」の全文です。

# Blueprint(pyファイルを分割するための関数)をインポート
from flask import Blueprint

#「app」を「Blueprint()」を使って定義
#「blog」の部分は、url_for("blog.top")で使用
app = Blueprint('blog', __name__, template_folder='templates')


# 必要なモジュールをインポート
import os
from datetime import datetime, timedelta, timezone
from flask import Flask, session, redirect, url_for, render_template,request
#「/blog/models/models.py」の「WikiContent」と「User」を呼び出す
from blog.models.models import WikiContent,User
#「/blog/models/database.py」の「db_session」を呼び出す
from blog.models.database import db_session

from werkzeug.utils import secure_filename

# ハッシュ化するための関数をインポート(パスワードの暗号化用)
# 今回はsha256という暗号化方式を使用
import key
from hashlib import sha256


#「/」へアクセスがあった場合
@app.route('/')
def index():  # 一覧画面
    if "user_name" in session:
        # セッションがログイン状態であれば「blog/index.html」を返す
        name = session["user_name"]
        all_diary= WikiContent.query.all()
        return render_template('blog/index.html',name=name, all_diary=all_diary)
    else:
        # ログイン状態でなければ「/blog/top」ページへ移動
        return redirect(url_for("blog.top"))

#「/logout」へアクセスがあった場合「/blog/top」ページへ移動
@app.route('/logout')
def logout():
    session.pop("user_name", None)
    return redirect(url_for("blog.top",status="logout"))

#「/create」へアクセスがあった場合「blog/edit.html」を返す
@app.route('/create')
def create():  #新規作成
    return render_template('blog/edit.html')

#「/diarysave」へアクセスがあった場合「/blog/index」ページへ移動
@app.route('/diarysave', methods=['post'])
def diarysave():  #保存
    image_url = ""
    # 画像ファイルを保存
    if request.files['image']:
        f = request.files['image']
        filepath = 'static/images/' + secure_filename(f.filename)
        f.save(filepath)
        filepath = '/' + filepath
        image_url = filepath
    # データベースに保存
    title = request.form["title"]
    comment = request.form["comment"]
    # JSTタイムゾーンを作成
    jst = timezone(timedelta(hours=9), 'JST')
    date = datetime.now(jst)
    content = WikiContent(title,image_url,comment,date)
    db_session.add(content)
    db_session.commit()
    return redirect(url_for("blog.index"))


@app.route('/all_delete')
def all_delete():  #全削除
    WikiContent.query.delete()
    db_session.commit()
    return redirect(url_for("blog.index"))


# top
#「/login」へアクセスがあった場合
@app.route("/login",methods=["post"])
def login():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合
        #「パスワード」が一致するか確認(「key.SALT」で暗号化)
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            #「パスワード」が一致した場合「/blog/index」ページへ移動
            session["user_name"] = user_name
            return redirect(url_for("blog.index"))
        else:
            #「パスワード」が一致しない場合「/blog/top」ページへ移動
            return redirect(url_for("blog.top",status="wrong_password"))
    else:
        #「ユーザー名」が存在しなかった場合「/blog/top」ページへ移動
        return redirect(url_for("blog.top",status="user_notfound"))

#「/registar」へアクセスがあった場合
# 新規登録画面で「新規登録」ボタンが押された時
@app.route("/registar",methods=["post"])
def registar():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合「/blog/newcomer」ページへ移動
        return redirect(url_for("blog.newcomer",status="exist_user"))
    else:
        #「ユーザー名」が存在しなかった場合
        #「ユーザー名」「パスワード」(「key.SALT」で暗号化)を
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        # データベースに追加して保存
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        #「/blog/index」ページへ移動
        return redirect(url_for("blog.index"))

#「/top」へアクセスがあった場合「blog/top.html」を返す
@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("blog/top.html",status=status)

#「/newcomer」へアクセスがあった場合「blog/newcomer.html」を返す
@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("blog/newcomer.html",status=status)

 

まとめ

http://startpython.pythonanywhere.com/blog/

こちらから実際に操作できますのでお試しください。(ユーザー名・パスワードは普段お使いのもの以外にしてください。セキュリティーは保証できません)
ユーザー名とパスワードを何も入れずにログインするとゲストでログインできます。

 

次回は編集と削除の機能を付けたいと思います。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村