便利で快適な入力フォーム開発に特化したJavaScriptライブラリ「InputManJS(インプットマンJS)」の最新バージョン「V5J」では、チャットやフォーラム、会話アプリなどで見られる会話機能のUIが構築できる「コメントコンポーネント(GcComment)」を追加しました。
今回はコメントコンポーネントとPythonのWebフレームワーク「FastAPI」を使用して、バックエンドのSQLiteのデータベースと連携する、コメント機能付きのアプリケーションを作成してみたいと思います。
目次
開発環境
今回は開発環境として以下を使用します。
- Python 3.13.0
- FastAPI 0.115.5
- Visual Studio Code
- Live Server(Visual Studio Code拡張機能)
バックエンド(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です)。
続けて同フォルダに「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ドキュメントが表示されます。
また、プロジェクトフォルダのルート直下に自動で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」を実行します。
実行後、ブラウザ上にコメントコンポーネントが組み込まれたWebページが表示されます。
※ あらかじめ先ほど作成したWeb APIを起動しておいて下さい。
動作確認
エディタからコメントやリアクションを登録すると、バックエンドのAPIと連携してデータベースにコメント情報等が登録され、画面をリロードしても登録したコメント情報がきちんと表示されます。
コメントの修正や、リアクションの削除等の変更もきちんと反映されます。
親コメントを削除すると、それに紐づく子コメントも削除されます。
コメントコンポーネントのデータベース連携については以下のデモアプリケーションもご参考ください。
さいごに
今回はWebアプリケーションにコメント機能を組み込むことができるInputManJSの「コメントコンポーネント(GcComment)」で、PythonのWebフレームワーク「FastAPI」で作成したWeb APIと連携してコメント機能付きのアプリケーションの作成方法を解説しました。次回は双方向のリアルタイム通信機能を追加し、複数人がリアルタイムで会話ができるチャットアプリケーションの作成方法を解説します。
なお、今回ご紹介したコメントコンポーネントの機能はほんの一部です。製品サイトでは、InputManJSのコメントコンポーネントの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。
また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。