InputManJSとFastAPIでリッチなコメント機能付きアプリを作成する

便利で快適な入力フォーム開発に特化したJavaScriptライブラリ「InputManJS(インプットマンJS)」の最新バージョン「V5J」では、チャットやフォーラム、会話アプリなどで見られる会話機能のUIが構築できる「コメントコンポーネント(GcComment)」を追加しました。

今回はコメントコンポーネントとPythonのWebフレームワーク「FastAPI」を使用して、バックエンドのSQLiteのデータベースと連携する、コメント機能付きのアプリケーションを作成してみたいと思います。

開発環境

今回は開発環境として以下を使用します。

バックエンド(Web API)の作成

コメントコンポーネントはサーバー(データベース)側との連携のためのインターフェースを用意しています。APIを介してコメントコンポーネントとデータベースとの双方向のデータバインディングを実現します。

サーバー連携用のインターフェース

今回は連携するサーバー側のWeb APIをPythonのWebフレームワーク「FastAPI」で作成していきます。FastAPIの概要や導入方法は以下の記事もご参考ください。

Web APIの仕様

コメントコンポーネントではコメント情報、ユーザー情報、リアクション情報の計3つのテーブルを使用します。以下に簡単に今回作成するWeb APIの仕様をまとめます。今回は最小限の機能を実装していきますので、使用するコメントコンポーネントの機能によっては追加実装が必要になる場合がある点をご注意ください。

コメント情報取得(GET)

コメントの情報を取得します

リクエストURL
  • http://localhost:8000/comments
リクエストパラメータ
  • なし
正常時のレスポンス(Content-Type:application/json)
フィールド名説明
hasMore動的読み込み機能を使用する際に使用。今回は未使用なので固定で「False」を返却。
commentsコメント情報を含む配列データを返却

commentsの配列には以下のようなデータを返却します。

フィールド名説明
idコメントごとに付与されるID
parentCommentId返信コメントを登録した場合に設定される、親となるコメントのID
contentコメントの本文
userIdコメントしたユーザーのID
mentionInfoメンション機能で使用するメンションしたユーザーの情報。今回は未使用。
postedTimeコメントの登録日時
updateTimeコメントの更新日時

コメント情報登録(POST)

コメントの情報を登録します

リクエストURL
  • http://localhost:8000/comments
リクエストパラメータ(Content-Type:multipart/form-data)
フィールド名説明
parentCommentId返信コメントを登録した場合に設定される、親となるコメントのID
contentコメントの本文
userIdコメントしたユーザーのID
mentionInfoメンション機能で使用するメンションしたユーザーの情報。今回は未使用。
正常時のレスポンス(Content-Type:application/json)
フィールド名説明
id登録したコメントのID
parentCommentId返信コメントを登録した場合に設定される、親となるコメントのID
contentコメントの本文
userIdコメントしたユーザーのID
mentionInfoメンション機能で使用するメンションしたユーザーの情報。今回は未使用。
postedTimeコメントの登録日時
updateTimeコメントの更新日時

コメント情報更新(PUT)

コメントの情報を更新します

リクエストURL
  • http://localhost:8000/comments
リクエストパラメータ(Content-Type:multipart/form-data)
フィールド名説明
id更新するコメントのID
parentCommentId返信コメントを登録した場合に設定される、親となるコメントのID
newContent更新後のコメントの本文
userIdコメントしたユーザーのID
mentionInfoメンション機能で使用するメンションしたユーザーの情報。今回は未使用。
正常時のレスポンス(Content-Type:application/json)
フィールド名説明
id更新したコメントのID
parentCommentId返信コメントを登録した場合に設定される、親となるコメントのID
content更新したコメントの本文
userIdコメントしたユーザーのID
mentionInfoメンション機能で使用するメンションしたユーザーの情報。今回は未使用。
postedTimeコメントの登録日時
updateTimeコメントの更新日時

コメント情報削除(DELETE)

コメントの情報を削除します

リクエストURL
  • http://localhost:8000/comments
クエリパラメータ
フィールド名説明
commentId削除するコメントのID
正常時のレスポンス(Content-Type:application/json)

「true」を返却します

ユーザー情報取得(GET)

ユーザーの情報を取得します

リクエストURL
  • http://localhost:8000/users
クエリパラメータ
フィールド名説明
id参照するユーザーのID
正常時のレスポンス(Content-Type:application/json)
フィールド名説明
idユーザーのID
usernameユーザーの名前
avatarユーザーのアイコン画像のパス(URL)

リアクション情報取得(GET)

リアクションの情報を取得します

リクエストURL
  • http://localhost:8000/reactions
クエリパラメータ
フィールド名説明
commentIdリアクションしたコメントのID
userIdリアクションしたユーザーのID
正常時のレスポンス(Content-Type:application/json)
フィールド名説明
reactionCharリアクションの絵文字
countリアクションの件数
currentUserReacted現在のユーザーのリアクションかどうかのフラグ

リアクション情報登録(POST)

リアクションの情報を登録します

リクエストURL
  • http://localhost:8000/reactions
リクエストパラメータ(Content-Type:multipart/form-data)
フィールド名説明
reactCharリアクションの絵文字
commentIdリアクションしたコメントのID
userIdリアクションしたユーザーのID
正常時のレスポンス(Content-Type:application/json)

「true」を返却します

リアクション情報削除(DELETE)

リアクションの情報を削除します

リクエストURL
  • http://localhost:8000/reactions
クエリパラメータ
フィールド名説明
commentId削除するリアクションのコメントのID
userId削除するリアクションを行ったユーザーのID
reactChar削除するリアクションの絵文字
正常時のレスポンス(Content-Type:application/json)

「true」を返却します

FastAPIでWeb APIの作成

それでは早速Web APIを作成していきます。まずはvenvを使って新しく「fastapi-comment-api」という仮想環境を作成します。
※ Python環境の構築方法はこちらの記事をご覧ください。

python -m venv fastapi-comment-api

「fastapi-comment-api」フォルダに移動し、仮想環境を有効化します。

cd fastapi-comment-api
Scripts\activate

FastAPIとASGI Webサーバの「Uvicorn」、Pythonで使えるORMの「SQLAlchemy」、さらにPythonでフォームデータを扱うために「Python-Multipart」をpip経由でインストールします。

pip install fastapi uvicorn sqlalchemy python-multipart

インストールが完了したらプロジェクトのルートに「app」フォルダを作成し、「__init__.py」ファイルを作成します(中身は空でOKです)。

__init__.pyファイルの配置

続けて同フォルダに「database.py」ファイルを作成し、データベース接続の設定を記載します。SQLAlchemyでSQLiteを使用する場合、外部キー制約がデフォルトでは有効にならないのでPragmaステートメントで使用して明示的に有効化します。

from sqlalchemy import create_engine, event
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.engine import Engine

DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
    cursor = dbapi_connection.cursor()
    cursor.execute("PRAGMA foreign_keys=ON")
    cursor.close()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

次に「models.py」を作成し、SQLAlchemyのモデル定義を記載します。commentsテーブルのparentCommentIdと、reactionsテーブルのcommentIdに対して外部キーの設定を行い、コメントが削除された場合に、関連する子コメント情報やリアクション情報が連動して削除されるように設定します。

from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from .database import Base

class Comment(Base):
    __tablename__ = "comments"
    id = Column('id', Integer, primary_key=True)
    parentCommentId = Column('parentCommentId', Integer, ForeignKey("comments.id", ondelete="CASCADE"), nullable=True)
    content = Column('content', Text)
    userId = Column('userId', Integer)
    mentionInfo = Column('mentionInfo', Text, nullable=True)
    postedTime = Column('postedTime', DateTime)
    updateTime = Column('updateTime', DateTime)

class User(Base):
    __tablename__ = "users"
    id =  Column('id', Integer, primary_key=True)
    username = Column('username', String)
    avatar = Column('avatar', String)

class Reaction(Base):
    __tablename__ = "reactions"
    id = Column('id', Integer, primary_key=True)
    commentId = Column('commentId', Integer, ForeignKey("comments.id", ondelete="CASCADE"))
    userId = Column('userId', Integer)
    reactionChar = Column('reactionChar', Text)

次に「schemas.py」を作成し、バリデーションなどを担うPydanticのスキーマ定義を記載します。

from pydantic import BaseModel
from typing import Union

class CommentIn(BaseModel):
    userId: int
    parentId: Union[int, str, None] = None
    content: str
    mentionInfo: Union[str, None] = None   
        
class ReactionIn(BaseModel):
    reactionChar: str
    commentId: int
    userId: int

最後にCRUD処理を行うアプリケーション本体の「main.py」を作成します。

from fastapi import Depends, FastAPI, Form, HTTPException, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from datetime import datetime
from typing import Union
from .database import engine, get_db
from . import models, schemas


models.Base.metadata.create_all(bind=engine)

# コメント情報取得のヘルパー関数
def get_comment(id: int, db_session: Session):
    return db_session.query(models.Comment).filter(models.Comment.id == id).first()

# ユーザー情報取得のヘルパー関数
def get_user(id: int, db_session: Session):
    return db_session.query(models.User).filter(models.User.id == id).first()

app = FastAPI()

# CORS対応
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"], 
    allow_headers=["*"] 
)

# Commentを全件取得
@app.get("/comments")
def read_comments(db: Session = Depends(get_db)):
    comments = db.query(models.Comment).all()
    return {
        "hasMore": False,
        "comments": [
            {
                "id": comment.id,
                "parentCommentId": comment.parentCommentId,
                "content": comment.content,
                "postedTime": comment.postedTime.strftime("%Y/%m/%d %H:%M:%S"),
                "updateTime": comment.updateTime.strftime("%Y/%m/%d %H:%M:%S"),
                "userId": comment.userId,
                "mentionInfo": comment.mentionInfo,
            }
            for comment in comments
        ],
    }

# Commentを登録
@app.post("/comments")
async def create_comment(
    userId: int = Form(...),
    parentId: Union[int, str, None] = Form(None),
    content: str = Form(...),
    mentionInfo: Union[str, None] = Form(None),
    db: Session = Depends(get_db)
):
    formdata = schemas.CommentIn(
        userId=userId,
        parentId=parentId,
        content=content,
        mentionInfo=mentionInfo,
    )

    comment = models.Comment(
        userId=formdata.userId,
        parentCommentId = None if formdata.parentId == 'undefined' else formdata.parentId,
        content=formdata.content,
        mentionInfo=formdata.mentionInfo,
        postedTime=datetime.now(),
        updateTime=datetime.now()
    )

    db.add(comment)
    db.commit()
    db.refresh(comment)
    return comment

# Commentを更新
@app.put("/comments")
async def update_comment(
    id: int = Form(...),
    userId: int = Form(...),
    parentCommentId: Union[int, str, None] = Form(None),
    newContent: str = Form(...),
    mentionInfo: Union[str, None] = Form(None),
    db: Session = Depends(get_db)
):
    formdata = schemas.CommentIn(
        userId=userId,
        parentId=parentCommentId,
        content=newContent,
        mentionInfo=mentionInfo,
    )

    comment = models.Comment(
        userId=formdata.userId,
        parentCommentId=formdata.parentId,
        content=formdata.content,
        mentionInfo=formdata.mentionInfo,
        updateTime=datetime.now()
    )

    db_comment = get_comment(id,db)
    if db_comment is None:
        raise HTTPException(status_code=404, detail="Comment not found")
    else:
        db_comment.userId = comment.userId
        db_comment.parentCommentId = None if comment.parentCommentId == 'undefined' else comment.parentCommentId
        db_comment.content = comment.content
        db_comment.mentionInfo = comment.mentionInfo    
        db_comment.updateTime = comment.updateTime    
    
        db.commit()
        db.refresh(db_comment)
        return db_comment

# Commentを削除
@app.delete("/comments")
def delete_comment(commentId: int, db: Session = Depends(get_db)):
    db_comment = get_comment(commentId,db)
    if db_comment is None:
        raise HTTPException(status_code=404, detail="Comment not found")
    else:
        db_comment = db.query(models.Comment).filter(models.Comment.id == commentId).delete()
        db.commit()
        return True

# Userを取得
@app.get("/users")
def read_user(id: int, db: Session = Depends(get_db)):
    user = get_user(id, db)
    return user

# Reactionを取得
@app.get("/reactions")
def read_reaction(commentId: int, userId: int, db: Session = Depends(get_db)):
    reactions = db.query(models.Reaction.reactionChar, func.count(models.Reaction.reactionChar).label("count")
        ).filter(models.Reaction.commentId == commentId).group_by(models.Reaction.reactionChar).all()

    user_reactions = db.query(models.Reaction.reactionChar).filter(models.Reaction.commentId == commentId, models.Reaction.userId == userId).all()
    user_reacted_chars = {reaction[0] for reaction in user_reactions}

    reaction_info = [
        {
            "reactionChar": reaction[0],
            "count": reaction[1],
            "currentUserReacted": reaction[0] in user_reacted_chars
        }
        for reaction in reactions
    ]

    return reaction_info

# Reactionsを登録
@app.post("/reactions")
async def create_reaction(
    reactChar: str = Form(...),
    commentId: int = Form(...),
    userId: int = Form(...),
    db: Session = Depends(get_db)
):
    formdata = schemas.ReactionIn(
        reactionChar=reactChar,
        commentId=commentId,
        userId=userId,
    )

    reaction = models.Reaction(
        reactionChar=formdata.reactionChar,
        commentId=formdata.commentId,
        userId=formdata.userId,
    )

    db.add(reaction)
    db.commit()
    db.refresh(reaction)
    return True

# Reactionを削除
@app.delete("/reactions")
def delete_reaction(commentId: int, userId: int, reactChar: str, db: Session = Depends(get_db)):
    db_reaction = db.query(models.Reaction).filter(models.Reaction.userId == userId, models.Reaction.commentId == commentId, models.Reaction.reactionChar == reactChar).delete()
    if db_reaction is None:
        raise HTTPException(status_code=404, detail="Reaction not found")
    else:
        db.commit()
        return True

# リクエストエラー時のハンドリング
@app.exception_handler(RequestValidationError)
async def handler(request:Request, exc:RequestValidationError):
    print(exc)
    return JSONResponse(content={}, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)

動作確認

完成したら以下のコマンドでAPIを起動します。

uvicorn app.main:app --reload

起動後、「http://127.0.0.1:8000/docs」にアクセスすると、自動生成されたOpenAPIのAPIドキュメントが表示されます。

OpenAPIのドキュメント

また、プロジェクトフォルダのルート直下に自動でSQLiteのデータベースファイル「test.db」が作成されるので、「DB Browser for SQLite」などで開くと、comments、users、reactionsの3つのテーブルが作成されていることが確認できます。

作成されたテーブルの確認

あらかじめDB Browser for SQLite上で以下のSQLを実行し、ユーザー情報を登録しておきます。

INSERT INTO USERS (id, username, avatar) VALUES (1, "森上 偉久馬", "./img/avatar1.png");
INSERT INTO USERS (id, username, avatar) VALUES (2, "葛城 孝史", "./img/avatar2.png");
INSERT INTO USERS (id, username, avatar) VALUES (3, "加藤 泰江", "./img/avatar3.png");
INSERT INTO USERS (id, username, avatar) VALUES (4, "川村 匡", "./img/avatar4.png");
INSERT INTO USERS (id, username, avatar) VALUES (5, "松沢 誠一", "./img/avatar5.png");
INSERT INTO USERS (id, username, avatar) VALUES (6, "成宮 真紀", "./img/avatar6.png");

フロントエンドの作成

次に先ほど作成したAPIと連携するフロントエンド側のアプリケーションを作成していきます。

事前準備

InputManJSの使用にはInputManJSのモジュールを環境にインストールする必要があります。CDNを参照したり、npmなどから入手したりする方法もありますが、今回は環境に直接InputManJSのモジュールを配置していきます。あらかじめInputManJSの製品版かトライアル版をご用意ください。トライアル版は以下より無償で入手可能です。

製品版、またはトライアル版をダウンロードしたら、ZIPファイルを解凍し、以下のファイルを環境にコピーします。

  • comment/scripts/gc.inputman.comment.ja.js
  • comment/css/gc.inputman.comment.css

また、「img」フォルダを作成し、コメントを入力するユーザーのアイコン画像を配置します。今回の記事で使用するアイコン画像はこちらからダウンロード可能です。

コピーしたファイルはそれぞれ以下のように配置します。

コピーしたファイルの配置場所

コメントコンポーネントの参照

まずはコメントコンポーネントを使うのに必要なライブラリの参照設定をHTMLファイルに追加します。コメントコンポーネントのモジュールのほか、初期化やAPIとの接続設定などの各種処理を記載する「app.js」への参照も追加します。
※ CDNから参照する場合はコメントアウトされている部分とライブラリの参照先を入れ替えてください。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>コメントコンポーネントサンプル</title>

    <!-- ローカルのライブラリを参照する場合 -->
    <link rel="stylesheet" href="css/gc.inputman.comment.css" />
    <script src="scripts/gc.inputman.comment.ja.js"></script>

    <!-- CDNからライブラリを参照する場合 -->
    <!--
    <link rel="stylesheet" href="https://cdn.mescius.com/inputmanjs/hosted/comment/css/gc.inputman.comment.css
" />
    <script src="https://cdn.mescius.com/inputmanjs/hosted/comment/scripts/gc.inputman.comment.ja.js"></script>
    -->

    <script src="scripts/app.js"></script>
</head>

<body>
</body>

</html>

コメントコンポーネントの組み込み

次にコメントコンポーネントをWebページに組み込んでいきます。「index.html」の中で、タグボックスコントロールを表示する領域を<div>タグで定義します。

・・・(中略)・・・
<body>
    <div id="gcComment"></div>
</body>

</html>

続いて「scripts/app.js」にコメントコンポーネントの初期化処理を記載します。dataSourceオプションのenableオプションをtrueに設定し、バックエンドとのAPI連携を有効化します。また、remoteオプションで、各APIのエンドポイントを設定します。
※ ライセンスキーを設定しない場合トライアル版を示すメッセージが表示されます。ライセンスキーの入手や設定方法についてはこちらをご覧ください。

GC.InputMan.LicenseKey = 'ここにInputManJSのライセンスキーを設定します';

document.addEventListener('DOMContentLoaded', () => {

        const gcComment = new GC.InputMan.GcComment(document.getElementById('gcComment'), {
        dataSource: {
            enabled: true,
            remote: {
                comments: {
                    read: {
                        url: `http://localhost:8000/comments`,
                    },
                    create: {
                        url: `http://localhost:8000/comments`,
                    },
                    update: {
                        url: `http://localhost:8000/comments`,
                    },
                    delete: {
                        url: `http://localhost:8000/comments`,
                    }
                },
                users: {
                    read: {
                        url: `http://localhost:8000/users`,
                        schema: {
                            dataSchema: {
                                name: 'username'
                            }
                        }
                    }
                },
                reactions: {
                    read: {
                        url: `http://localhost:8000/reactions`,
                    },
                    create: {
                        url: `http://localhost:8000/reactions`,
                    },
                    delete: {
                        url: `http://localhost:8000/reactions`,
                    }
                },
            }
        },
        editorConfig: {
            height: 150,
        },
        commentMode: GC.InputMan.GcCommentMode.ThreadMode,
        userInfo: {
            id: "1",
            username: "森上 偉久馬",
            avatar: 'img/avatar1.png',
            avatarType: 'square',
        }
    });
});

なお、今回は以下のようにユーザー情報を固定にしているので、1人のユーザーしかコメントが登録できませんが、先ほど作成したusersのAPIからユーザー情報を取得するなどして、ログインするユーザーに応じて動的にuserInfoを設定することで、アプリ上で複数人がコミュニケーションをすることが可能になります。

・・・(中略)・・・
        userInfo: {
            id: "1",
            username: "森上 偉久馬",
            avatar: 'img/avatar1.png',
            avatarType: 'square',
        }
    });
});

以上でコメントコンポーネントを使用する準備は完了です。Visual Studio Code上で「index.html」を右クリックして、「Open with Live Server」を実行します。

Live Serverを実行

実行後、ブラウザ上にコメントコンポーネントが組み込まれたWebページが表示されます。
※ あらかじめ先ほど作成したWeb APIを起動しておいて下さい。

コメントコンポーネントをWebページに組み込み

動作確認

エディタからコメントやリアクションを登録すると、バックエンドのAPIと連携してデータベースにコメント情報等が登録され、画面をリロードしても登録したコメント情報がきちんと表示されます。

コメントの修正や、リアクションの削除等の変更もきちんと反映されます。

親コメントを削除すると、それに紐づく子コメントも削除されます。

コメントコンポーネントのデータベース連携については以下のデモアプリケーションもご参考ください。

「データソース」のデモを見る

さいごに

今回はWebアプリケーションにコメント機能を組み込むことができるInputManJSの「コメントコンポーネント(GcComment)」で、PythonのWebフレームワーク「FastAPI」で作成したWeb APIと連携してコメント機能付きのアプリケーションの作成方法を解説しました。次回は双方向のリアルタイム通信機能を追加し、複数人がリアルタイムで会話ができるチャットアプリケーションの作成方法を解説します。

なお、今回ご紹介したコメントコンポーネントの機能はほんの一部です。製品サイトでは、InputManJSのコメントコンポーネントの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。

また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。

\  この記事をシェアする  /