今日のPython 第27回「ulidでユニーク(一意な) IDを採番してデータを管理」W010

2024年12月31日火曜日

Python_WEBアプリ編 公開

t f B! P L
記事作成:2024/12/31

管理番号:W010

はじめに

今、PythonでWEBアプリの作成を進めています。少しづつ機能を追加して、ローカルの仮想環境で動作確認をするという作業を地道に行っている状況です。

そして、進捗がある都度、このブログで紹介しています。

前回までの状況

前回は、ログイン機能の追加にチャレンジしてみました。

Flask-loginでログイン機能を追加して

flask_sqlalchemyでログインユーザーをデータベース管理しています。

■現在の画面表示■

改善が必要な点

家計簿情報は、あまり人に見られたくない情報です。

前回、ログイン機能を追加したことで、誰でも家計簿情報を閲覧できてしまうという問題は解決しました。

しかし、まだ解決しないといけない問題が残っています。

ログインユーザー間で、他のユーザーの家計簿情報が閲覧できてしまう、という点です。

現在、JSONファイルの辞書型リストを家計簿用のデータベースとして代用しています。

この一つのデータベースに複数のユーザーがログインして、その全てのユーザーが、このデータベースに登録されている全ての情報を閲覧することが出来てしまうという状態です。

ログインユーザー間でも、閲覧制限をかけて、各自の固有の情報だけが閲覧できる仕様にする必要があります。


今回の実施内容

そこで、今回は、ログインしたユーザーが、それぞれ、自分の家計簿情報だけを閲覧することができるようにしたいと思います。

今、家計簿用のデータベースは一つ。これにどのように、ユーザー毎の閲覧制限をかけるか、考えてみました。

思いついたのが、各データに

「ユーザー名」と「管理番号」

の情報を持たせることにより、擬似的にユーザー毎にデータベースが存在するように見せようというものです。

もう少し具体的に説明しますと

ユーザーがログインしたときに、家計簿用のデータベースの中から、そのユーザー名が登録されたデータのみを画面上に表示します。

こうすることでログインしたユーザーは、画面上に表示された自分の情報だけしか閲覧することが出来なくなります。

そして、管理番号は、各データを削除する場合に使用します。

削除したいデータの管理番号を指定して削除するのですが、ここで少し問題があります。

家計簿用のデータベースとして使用しているJSONファイルの辞書型リストに、どのように管理番号を付与するか、という点です。

本物のデータベースでは、各データに管理番号として重複しないIDを採番してくれる機能があります。

しかし、JSONの辞書型リストにはそのような機能は無いようですので、別の方法で重複しないIDを採番する必要があります。

そこで今回は、ULID というユニーク(一意な)ID を生成するための仕様/規格で重複の無い管理番号を付与することにしました。

以下に今回のプログラムの修正のうち主なものを紹介します。


関連ファイルの説明

プログラムの修正の紹介は、関連するファイルとそのコード修正について部分的に抜粋して説明していますので、全体のどの部分を修正しているのかを把握し辛いと思います。

その点を緩和するために、このプロジェクトで使用するファイルやフォルダ構成を、別ページで解説しています。

必要に応じてご覧ください。(→参考)

では始めます。



ライブラリインストール

必要なライブラリをインストールします。

今回は、python-ulidをインストールです。

前述しましたULIDのpythonライブラリが、python-ulidです。

VScode の 仮想環境に入って、



PS C:\Py\Project\MyPj> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process PS C:\Py\Project\MyPj> .\FlaskPj\Scripts\activate



pip でインストールします。


(FlaskPj) PS C:\Py\Project\MyPj> pip install python-ulid



インストール後、pip listで確認



(FlaskPj) PS C:\Py\Project\MyPj> pip list




無事インストールされていれば以下のように表示されます。


Package           Version
----------------- -------

python-ulid        3.0.0



ライブラリのインポート


■main.py■


from flask import Flask, render_template, request, redirect, Response, url_for # --- 追加10 url_for

from ulid import ULID  #--- 追加11 # ULIDクラスをインポート



■説明等■

さきほど、インストールした「ulid」と「url_for」を追加でインポートします。


データ初期設定の変更


■KDpay.py 収支データにユーザー名と管理番号を追加■


class Pay:
    def __init__(self, pay_data):
        # ペイ(収支)データの初期化
      (略)
        self.id      = pay_data["id"] #--- 追加10-2
        self.user    = pay_data["user"] #--- 追加10



■説明等■
Payクラスにユーザー名としてuser、管理番号としてid、を追加します。


■main.py 管理番号用のユニーク(一意な)IDを作成する■


# ------------------------------------------------------------------------------ #
# 家計簿
# ------------------------------------------------------------------------------ #
@app.route('/kakeibo/<string:user_name>', methods=['GET', 'POST']) #--- 修正10
@login_required # ログインしているユーザーのみに制限 #--- 追加 9  ★★テストのときはコメントアウト★★ #

def kd( user_name ): #--- 修正10

    (略)
 
    # 一意なIDオブジェクト作成
    main_key = str(ULID()) #--- 追加10-3
 
    (略)
 
    # ログイン中のユーザーの家計簿リストを作成
    search_list = [ item for item in  json_load_list if item['user'] == user_name ] #--- 追加10



■説明等■
python-ulidを使用します。
main_key = str(ULID()) で一意なIDオブジェクトを作成



■main.py 収支データ作成時にユーザー名と管理番号を追加登録■

    # ------------------------------------------------------------------------------ #
    ####    HTTPリクエスト
    ####   POSTメソッド受信
    ####  「送信」   ボタンが押されたときの処理 # --- 追加6-2
    if request.method == 'POST':  
        # 画面上で入力された情報を取得し、辞書データを作成
        add_dict = {"month"   : request.form.get('month'),
                    "day"     : request.form.get('day'),
                    "content" : request.form.get('content'),
                    "amount"  : request.form.get('amount'),
                    "id"      : main_key , # --- 追加10-2
                    "user"    : user_name # --- 追加10
                   }
         
        # 辞書データを辞書型リスト(PayManagerオブジェクト)に追加
        pay_manager.add_pay(add_dict) #--- 修正7-5 (前)  json_load_list.append(add_dict)
 
        # JSONファイルに書き込み # --- 修正7-6
        with open(file_path, mode='w',encoding="UTF-8") as open_json:
            updated_data_list=[]
            for pay in pay_manager.pay_data_list:
                updated_data_list.append({
                                          "month"   : pay.month,              
                                          "day"     : pay.day,
                                          "content" : pay.content,
                                          "amount"  : pay.amount,
                                          "id"      : pay.id, # --- 追加10-2
                                          "user"    : pay.user # --- 追加10
                                        })
            json.dump(updated_data_list, open_json ,indent=4,ensure_ascii=False)
         
        updated_search_list = [ item for item in  updated_data_list if item['user'] == user_name ] #--- 追加10
         
        # 更新した辞書型リストを送信し、HTMLをレンダリング
        return render_template('kakeibo_test_json/kd.html', title='kakeibo',user_name = user_name , pay_list = updated_search_list ) #--- 修正10
 


■説明等■
家計簿ページの画面上で入力された収支データを add_dict で辞書データにします。
このとき、ログイン時に取得したユーザー名を user に、
main_key = str(ULID()) で作成した一意なIDオブジェクトを管理番号として id に
格納します。

家計簿情報表示処理方法の変更


■main.py ログイン画面で入力されたログインユーザーのユーザー名を家計簿に送るための処理を追加■


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == "POST":
        username = request.form.get('username')
        password = request.form.get('password')
        # Userテーブルからusernameに一致するユーザを取得
        user = User.query.filter_by(username=username).first()
        if user.password == password: # hashを使用する場合(def signup()も修正必要箇所あり)差し替え→ # if check_password_hash(user.password, password):
            login_user(user)
 
            return redirect(url_for('kd', user_name = user.username)) # 家計簿へ #--- 修正10  # 修正前  # return redirect('/kakeibo')
         




■説明等■
ログイン画面で入力されたログインユーザーの情報「username」を、
リダイレクトで、家計簿(main.pyの「def kd()」)に送る処理を追加します
(return redirect(url_for('kd', user_name = user.username))の部分)


■main.py ログインユーザー限定の家計簿リストを作成する処理を追加■


# ------------------------------------------------------------------------------ #
# 家計簿
# ------------------------------------------------------------------------------ #
 
@app.route('/kakeibo/<string:user_name>', methods=['GET', 'POST']) #--- 修正10 # 修正前 # @app.route('/kakeibo', methods=['GET', 'POST']) #--- 修正6-1
@login_required # 閲覧制限、ログインしているユーザーのみに制限 #--- 追加 9  ★★テストのときはコメントアウト★★ #
 
def kd( user_name ): #--- 修正10 # 修正前 # def kd():
    # 既存ファイルを確認
    if not os.path.exists(file_path):
        # 無ければ作成
        with open( file_path, mode='w', encoding="UTF-8" ) as open_json:
            initial_list = [{
                            "month"   : []  , # "月"
                            "day"     : []  , # "日"
                            "content" : []  , # "内訳"
                            "amount"  : []  , # "金額"
                            "id"      : []  , # "ID"  #--- 追加10-2
                            "user"    : []    # "ユーザー"  #--- 追加10
            } ]
            json.dump( initial_list, open_json, indent=4, ensure_ascii=False)
 
    # ペイ(収支)データ管理用クラス(KDpay.py)オブジェクト作成  #--- 追加7
    pay_manager = PayManager()
 
    # 一意なIDオブジェクト作成
    main_key = str(ULID()) #--- 修正11 
 
    # JSONファイルを読み込んで、辞書型リストを作成
    with open( file_path, mode='r', encoding="UTF-8") as open_json:
        json_load_list = json.load(open_json)
 
    # 読み込んだファイルをペイ(収支)データ管理用クラスオブジェクトに追加 --- 修正7
    for pay_data in json_load_list:
        pay_manager.add_pay(pay_data)
 
    # ログイン中のユーザーの家計簿リストを作成
    search_list = [ item for item in  json_load_list if item['user'] == user_name ] #--- 追加10
 
 
    '''''''''''''''''
     HTTPリクエスト
     GETメソッド受信 # --- 修正6
    '''''''''''''''''
    if request.method == 'GET':
        # 更新した辞書型リストを送信し、HTMLをレンダリング        
        return render_template('kakeibo_test_json/kd.html', title='kakeibo',user_name = user_name , pay_list = search_list ) #--- 修正10 '''修正前''''''# return render_template('kakeibo_test_json/kd.html', title='kakeibo', pay_list = pay_manager.pay_data_list) #--- 修正7-4
 



■説明等■
main.pyの「def kd()」で「username」を受け取り、受け取ったユーザー名を基にログイン中のユーザーの家計簿リスト「search_list」を作成。
作成したリストを家計簿ページ「kd.html」へ送る処理をします。


■kd.html ログインユーザー限定の家計簿リストを表示する処理■



<table>
        <tr>
                <th>番号</th>
                <th></th>
                <th></th>
                <th>内訳</th>
                <th>金額</th>
        </tr>
        {% set cnt = [1] %} <!-- /////番号追加 # 修正7 /////////-->
        {% for pay in pay_list %}
        <tr>
                <td>{{ cnt[0] }}</td>
                <td>{{ pay.month}}</td>
                <td>{{ pay.day}}</td>
                <td>{{ pay.content }}</td> 
                <td>{{ pay.amount }}</td>
        </tr>
        {%- set _ = cnt.append(cnt[0] + 1) -%}<!-- /////加算した値をappendでリストに足して、加算前の値はpopで削除。 # 修正7 /////////-->
        {%- set _ = cnt.pop(0) -%}

        {% endfor %}
</table>




■説明等■
家計簿ページで、受け取った家計簿リスト「pay_list」を表示


データ追加時の処理方法の変更


■kd.html 家計簿ページで入力された情報をバックエンドに送る処理にユーザー名を追加■



<h1>家計簿アプリ</h1>
{{ user_name }}  <!-- // 追記10 //-->

<form name="pay_input_form" action="{{ url_for('kd', user_name = user_name) }}" method="POST"> <!-- // 修正10 //-->
        <label for="month">月:</label>
        <input id="month" name="month" type="text" size="1" />
        <label for="day">日:</label>
        <input id="day" name="day" type="text" size="1" />
        <label for="content">内訳:</label>
        <input id="content" name="content" type="text" size="5" />
        <label for="amount">金額:</label>
        <input id="amount" name="amount" type="text" size="5" />
        <input type="button" value="送信" onclick="formReset() " />
</form>




■説明等■
家計簿ページでフォームに入力された情報を、バックエンド(main.pyの「def kd(  )」)に送信する際に、同時にユーザーネーム(「user_name」)を送ります。


■main.py バックエンドで受け取ったデータでログインユーザー限定の家計簿リストを作成する処理を追加■



# ------------------------------------------------------------------------------ #
# 家計簿
# ------------------------------------------------------------------------------ #

@app.route('/kakeibo/<string:user_name>', methods=['GET', 'POST']) #--- 修正10
@login_required # ログインしているユーザーのみに制限 #--- 追加 9  ★★テストのときはコメントアウト★★ #

def kd( user_name ): #--- 修正10

    (略)            

    # ------------------------------------------------------------------------------ #
    ####    HTTPリクエスト
    ####   POSTメソッド受信
    ####  「送信」   ボタンが押されたときの処理 # --- 追加6-2
    if request.method == 'POST':  
        # 画面上で入力された情報を取得し、辞書データを作成
        add_dict = {"month"   : request.form.get('month'),
                    "day"     : request.form.get('day'),
                    "content" : request.form.get('content'),
                    "amount"  : request.form.get('amount'),
                    "id"      : main_key , # --- 追加10-2
                    "user"    : user_name # --- 追加10
                   }
         
        # 辞書データを辞書型リスト(PayManagerオブジェクト)に追加
        pay_manager.add_pay(add_dict) #--- 修正7-5 (前)  json_load_list.append(add_dict)
 
        # JSONファイルに書き込み # --- 修正7-6
        with open(file_path, mode='w',encoding="UTF-8") as open_json:
            updated_data_list=[]
            for pay in pay_manager.pay_data_list:
                updated_data_list.append({
                                          "month"   : pay.month,              
                                          "day"     : pay.day,
                                          "content" : pay.content,
                                          "amount"  : pay.amount,
                                          "id"      : pay.id, # --- 追加10-2
                                          "user"    : pay.user # --- 追加10
                                        })
            json.dump(updated_data_list, open_json ,indent=4,ensure_ascii=False)
         
        updated_search_list = [ item for item in  updated_data_list if item['user'] == user_name ] #--- 追加10
         
        # 更新した辞書型リストを送信し、HTMLをレンダリング

        return render_template('kakeibo_test_json/kd.html', title='kakeibo',user_name = user_name , pay_list = updated_search_list ) #--- 修正10
 






■説明等■

main.pyの「def kd()」で「user_name」を受け取り、

受け取ったユーザー名を基にログイン中のユーザーの家計簿リスト「updated_search_list」を作成

作成したリストを家計簿ページ「kd.html」へ送る処理を追加します。


データ削除時の処理方法の変更


■main.py■


# ------------------------------------------------------------------------------ #
# 家計簿
# ------------------------------------------------------------------------------ #
 
@app.route('/kakeibo/<string:user_name>', methods=['GET', 'POST']) #--- 修正10 # 修正前 # @app.route('/kakeibo', methods=['GET', 'POST']) #--- 修正6-1
@login_required # 閲覧制限、ログインしているユーザーのみに制限 #--- 追加 9  ★★テストのときはコメントアウト★★ #
 
def kd( user_name ): #--- 修正10 # 修正前 # def kd():
   
            (略) 

    '''''''''''''''''
     HTTPリクエスト
     POSTメソッド受信
    「削除」
       ボタンが押されたときの処理 # --- 追加 7
    '''''''''''''''''
 
    # POSTメソッド受信 & 入力画面で削除番号が入力されていれば
    if request.method == 'POST' and request.form.get('delete_number') != None :
        # 削除番号を取得、入力された削除番号を整数型に変換、リストの要素番号が0から始まるため1マイナス
        delete_target_id_number = int( request.form.get('delete_number') ) - 1  #--- 修正10-2  # 修正前 # number = int( request.form.get('delete_number') ) - 1
        # 削除番号を、ログイン中のユーザーの番号から全ユーザーのidに変換
        search_list_id_list = [d.get('id') for d in search_list] # ログイン中の個別ユーザーのidリスト #--- 追加10-2
        delete_target_id = search_list_id_list[delete_target_id_number] # ログイン中の個別ユーザーのidリスト中、削除するid  #--- 追加10-2
        json_load_list_id_list = [d.get('id') for d in json_load_list] # 全ユーザーのidリスト #--- 追加10-2
        delete_id_number = json_load_list_id_list.index(delete_target_id) # 全ユーザーのidリスト中、削除するidが何番目に存在するか確認  #--- 追加10-2
        # 全ユーザーの家計簿リストから対象となるidを持つデータを削除
        pay_manager.delete_pay(delete_id_number) #--- 修正10-2 #--- 修正前# pay_manager.delete_pay(number)
 
        # データ削除後のリストをJSONファイルに書き込み --- 修正7
        with open(file_path, mode='w',encoding="UTF-8") as open_json:
            updated_data_list=[]
            for pay in pay_manager.pay_data_list:
                updated_data_list.append({
                                        "month"   : pay.month,              
                                        "day"     : pay.day,
                                        "content" : pay.content,
                                        "amount"  : pay.amount,
                                        "id"      : pay.id , # --- 追加10-2
                                        "user"    : pay.user # --- 追加10
                                        })
            json.dump(updated_data_list, open_json ,indent=4,ensure_ascii=False)
        # データ削除後のリストから、ログイン中のユーザーの情報のみのリストを作成
        updated_search_list = [ item for item in  updated_data_list if item['user'] == user_name ] #--- 追加10
        # 更新した辞書型リストを家計簿画面に送信し、HTMLをレンダリング
        return render_template('kakeibo_test_json/kd.html', title='kakeibo',user_name = user_name , pay_list = updated_search_list ) #--- 修正10  #  '''修正前'''   # return render_template('kakeibo_test_json/kd.html', title='kakeibo', pay_list=pay_manager.pay_data_list) #--- 修正7-4
 
 



■説明等■
家計簿ページの画面に表示されているリストの中から削除するデータの番号を受け取り、管理番号に変換して、一致するデータを削除する処理を追加します。

以上が主な変更点の説明になります。


実施結果

ログインすると、ログイン中のユーザーの家計簿データのみが表示されます。


ログイン後の家計簿画面
前回と見た目はほとんど変わりません。
わかりにくいですが、「家計簿アプリ」の表示の下にログイン中のユーザー名を表示するようにしました。

「aaa」と表示されているのがわかりますか?
これが、ログインユーザー名です。


おわりに

以上、今回は家計簿の収支データにユーザー名と管理番号情報を追加することで一つのデータベースを各ユーザー毎に分類して各自のデータのみ閲覧出来るようにしてみました。

また一つ実用化に向けて課題の解決が出来て嬉しく思います。

早く皆さんにご利用いただけるレベルにしたいと思います。








このブログを検索

アーカイブ

カテゴリー

QooQ