【Python】AdminLTEとWebSocketでチャット機能を作ってみる その4(トーク切り替え)

2018年10月13日Python,開発

おはようございます。

前回に引き続き、チャットの実装をしていきます。
今回は、コンタクトリストを選択してチャット相手を切り替えられるようにしたいと思います。

プログラムは前回のものを流用します。
【Python】AdminLTEとWebSocketでチャット機能を作ってみる その3(データ表示、登録)

スポンサーリンク

画面の修正

初期表示はメッセージを表示せずに、
コンタクトリストをクリックしてメッセージを表示するように修正。

画面

main.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta http-equiv="content-type" content="text/html; charset=UTF-8">
	<title>チャットサンプル</title>
	<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
	<link rel="stylesheet" href="https:////maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
	<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
	<link rel="stylesheet" href="{{ static_url('css/AdminLTE.min.css') }}">
	<link rel="stylesheet" href="{{ static_url('css/style.css') }}">
	<link rel="stylesheet" href="{{ static_url('css/skins/skin-blue.min.css') }}">
</head>
<body class="hold-transition fixed">
	<form id="logoutForm" action="/logout" method="get">
		{% module xsrf_form_html() %}
	</form>
	<nav class="navbar navbar-default">
		<div class="container-fluid">
			<!-- ヘッダ情報 -->
			<div class="navbar-header" style="padding:15px;">
				<input type="hidden" id="user_id" value="{{ user_id }}" />
				<input type="hidden" id="user_name" value="{{ user_name }}" />
				ユーザー名:{{ user_name }}
			</div>
			<!-- リストの配置 -->
			<ul class="nav navbar-nav">
				<li class="active"><a href="#">チャット</a></li>
				<li><a href="#">メニュー1</a></li>
				<li><a href="#">メニュー2</a></li>
			</ul>
			<!-- ボタン -->
			<button type="button" class="pull-right btn btn-default navbar-btn">
				ログアウト&nbsp;<span class="fa fa-sign-out"></span>
			</button>
		</div>
	</nav>
	<section class="content container-fluid">
		<div class="row">
			<div class="col-xs-8">
				<div class="row">
					<div class="col-xs-8">
						<div id="chat-panel" class="box box-warning direct-chat direct-chat-warning box-solid" style="display:none;">
							<input type="hidden" id="room_no" />
							<div class="box-header with-border">
								<h3 class="box-title">チャットメッセージ</h3></span>
								<div class="box-tools pull-right">
									<span data-toggle="tooltip" title="new message" class="badge bg-red">1</span>
									<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
									<button type="button" class="btn btn-box-tool" data-toggle="tooltip" title="Contacts" data-widget="chat-pane-toggle">
										<i class="fa fa-comments"></i>
									</button>
									<button type="button" class="btn btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button>
								</div>
							</div>
							<div class="box-body">
								<div class="direct-chat-messages">
									<span class="blank-msg">チャットする相手を選択してください。</span>
								</div>
								<div class="direct-chat-contacts">
									<ul class="contacts-list">
									{% for user in relation_users %}
										<li id="user_{{ user['user_id'] }}">
											<a href="javascript:room_select('{{ user['room_no'] }}', '{{ user['user_id'] }}');">
												<img class="contacts-list-img" src="static/img/{{ user['icon'] }}" alt="User Image">
												<div class="contacts-list-info">
													<span class="contacts-list-name">
														{{ user['user_name'] }}
														<small class="contacts-list-date pull-right">{{ user['update_date'] }}</small>
													</span>
													<span class="contacts-list-msg">{{ user['message'] }}</span>
												</div>
											</a>
										</li>
									{% end %}
									</ul>
								</div>
							</div>
							<div class="box-footer">
								<form action="#" method="post">
									<div class="input-group">
										<textarea id="message" type="text" name="message" placeholder="メッセージを入力してください" class="form-control"></textarea>
										<span class="input-group-btn">
											<button id="sendButton" type="button" class="btn btn-disable btn-flat btn-sm" disabled>送信</button>
										</span>
									</div>
								</form>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</section>
	<script src="//ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
	<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.19/jquery-ui.min.js"></script>
	<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
	<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.js"></script>
	<script src="{{ static_url('js/adminlte.min.js') }}"></script>
	<script src="{{ static_url('js/script.js') }}"></script>
	<script>
		$(document).ready( function () {
			initialize();
		} );
	</script>
	</body>
</html>

スタイル

style.css

次のクラスを追加

.blank-msg {
    font-size: 28px;
    color: #cccccc;
}

 

プログラム

新規追加

まだ作成していなかったテーブル用のDAOを追加

Dao/MstRoomDao.py

from Dao.SqlClient import SqlClient


class MstRoomDao(SqlClient):
    """
    MSTチャットルームDAO
    """

    def select_next_room_no(self):
        """
        次の部屋番号を返します.
        :return:
        """
        sql = "SELECT MAX(ROOM_NO) + 1 AS ROOM_NO FROM MST_ROOM"

        data = super().select_one(sql)
        return data["ROOM_NO"]

    def create_new_room(self, room_no, room_name, user_id):
        """
        部屋を作成します
        :param data:
        :return:
        """
        record = [
            room_no
            , room_name
            , ""
            , super().get_datetime()
            , user_id
            , super().get_datetime()
            , None
            , None
        ]
        sql = "INSERT INTO MST_ROOM VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
        super().execute(sql, record)

Dao/MstUserRelationDao.py

from Dao.SqlClient import SqlClient


class MstUserRelationDao(SqlClient):
    """
    MSTユーザ関係DAO
    """

    def update_room_no(self, current_user, target_user, room_no):
        """
        部屋番号を更新します.
        :param current_user:
        :param target_user:
        :param room_no:
        :return:
        """
        sql = "UPDATE MST_USER_RELATION"
        sql += " SET"
        sql += "    ROOM_NO = %(room_no)s"
        sql += "    , UPDATE_USER = %(current_user)s"
        sql += "    , UPDATE_DATE = '" + super().get_datetime() + "'"
        sql += " WHERE ("
        sql += "    USER_ID = %(current_user)s AND"
        sql += "    USER_ID2 = %(target_user)s"
        sql += " ) OR ("
        sql += "    USER_ID = %(target_user)s AND"
        sql += "    USER_ID2 = %(current_user)s"
        sql += " )"

        super().execute(sql, {
            "room_no": room_no
            , "current_user": current_user
            , "target_user": target_user
        })

Dao/TblChatRoomMemberDao.py

from Dao.SqlClient import SqlClient


class TblChatRoomMemberDao(SqlClient):
    """
    チャットメンバーDAO
    """

    def insert_member(self, room_no, current_user_id, user_id):
        """
        チャットメンバーを登録します
        :param room_no:
        :param current_user_id:
        :param user_id:
        :return:
        """
        record = [
            room_no
            , user_id
            , current_user_id
            , super().get_datetime()
            , None
            , None
        ]

        sql = "INSERT INTO TBL_CHATROOM_MEMBER VALUES(%s,%s,%s,%s,%s,%s)"
        super().execute(sql, record)

既存クラスの修正

クエリを一部修正

Dao/MstUserDao.py

    def select_relation_user(self, user_id):
        """
        関係ユーザーを取得します
        :param user_id:
        :return:
        """

        sql = "SELECT"
        sql += "  R.ROOM_NO"
        sql += "  , M.ROOM_NAME"
        sql += "  , U.* "
        sql += "FROM"
        sql += "  MST_USER_RELATION R"
        sql += "  LEFT OUTER JOIN MST_ROOM M ON ("
        sql += "  	M.ROOM_NO = R.ROOM_NO"
        sql += "  )"
        sql += "  LEFT OUTER JOIN MST_USER U ON ("
        sql += "  	U.USER_ID = R.USER_ID OR"
        sql += "  	U.USER_ID = R.USER_ID2"
        sql += "  ) "
        sql += "WHERE"
        sql += "  U.USER_ID <> %(user_id)s AND ("
        sql += "  	R.USER_ID = %(user_id)s OR R.USER_ID2 = %(user_id)s"
        sql += "  )"

        result = []
        data = super().select(sql, {
            "user_id" : user_id
        })
        for r in data:
            result.append(self.mapping_data(r))

        return result

 

DAO利用宣言の追加

Main.py

from Dao.MstRoomDao import MstRoomDao
from Dao.MstUserRelationDao import MstUserRelationDao
from Dao.TblChatRoomMemberDao import TblChatRoomMemberDao

 

メイン処理を修正し、コンタクトリスト選択時の処理を追加

Main.py

class MainHandler(AuthBaseHandler):
    """
    初期表示処理
    """

    def initialize(self):
        logging.info("[MainHandler] initialize")

    @tornado.web.authenticated
    def get(self):
        logging.info("[MainHandler] get")
        user_id = self.get_current_user()
        user_dao = MstUserDao()
        user = user_dao.select_user(user_id)
        user_name = user['user_name']

        relation_users = user_dao.select_relation_user(user_id)

        self.render("main.html",
                    user_id=user_id,
                    user_name=user_name,
                    relation_users=relation_users)


class SelectHandler(AuthBaseHandler):
    """
    チャット相手を選択した際の処理
    """
    def initialize(self):
        logging.info("SelectHandler [initialize]")

    def post(self):

        param = json.loads(self.request.body)
        room_no = param["room_no"]
        current_user = self.get_current_user()
        target_user = param["user_id"]

        # まだ部屋が作成されていない場合
        if room_no == "0":

            # 部屋の作成
            room_dao = MstRoomDao()
            room_no = room_dao.select_next_room_no()
            room_dao.create_new_room(room_no, "個別", current_user)

            # メンバーの登録
            member_dao = TblChatRoomMemberDao()
            member_dao.insert_member(room_no, current_user, current_user)
            member_dao.insert_member(room_no, current_user, target_user)

            # 部屋番号の更新
            relation_dao = MstUserRelationDao()
            relation_dao.update_room_no(current_user, target_user, room_no)

        # メッセージの取得
        message_dao = TblChatMessageDao()
        chat_messages = message_dao.select_message_by_room_no(room_no)
        data = {
            "room_no": room_no
            , "user_id": target_user
            , "messages": chat_messages
        }

        self.write(json.dumps(data, ensure_ascii=False))


application = tornado.web.Application([
    (r"/login", AuthLoginHandler),
    (r"/logout", AuthLogoutHandler),
    (r"/main", MainHandler),
    (r"/chat", ChatHandler),
    (r"/select", SelectHandler),
],
    template_path=os.path.join(os.getcwd(),  "templates"),
    static_path=os.path.join(os.getcwd(),  "static"),
    login_url="/login",
    cookie_secret="adfaskljfwepmaldskf:as;k",
    xsrf_cookies=True
)

 

クライアント側の処理も修正。
初期表示、ソケット処理を修正、コンタクトリスト選択時の処理を追加

script.js

/**
 * 初期処理.
 */
function initialize() {

    // 通信ソケットオープン
    socket.onopen = function(data) {
        $("#status").css("color", "#FFFFFF");
        $("#status").text(" [オンライン]");
    }

    // 通信ソケットクローズ
    socket.onclose = function() {
        // 初期表示に戻してオフラインとする
        $(".direct-chat-messages").empty();
        $(".direct-chat-messages").append(
            $("<span>", {
                "class" : "blank-msg"
                , "text" : "ネットワークが繋がっていません。"
            })
        );
        $("#room_no").val(0);
    }

    // メッセージ受信
    socket.onmessage = function(e) {
        console.log(e.data);
        var data = JSON.parse(e.data);

        $(".direct-chat-messages").append(createMessage(data));
        $(".direct-chat-messages").animate({
            scrollTop: $(".direct-chat-messages")[0].scrollHeight
        }, 500);
    }

    // ボタンにイベントを追加
    $("#sendButton").click(function () {
        sendMessage();
    });

    // チャットの表示を一番下に
    $("#chat-panel").show();
    $(".direct-chat-messages")[0].scrollTop = $(".direct-chat-messages")[0].scrollHeight;
}

/**
 * メッセージを送信.
 */
function sendMessage() {

    // メッセージが未入力の場合は送信しない
    if(!$("#message").val()) {
        return;
    }

    // 送信日時
    var now = new moment();
    var send_date = now.format("YYYY-MM-DD HH:mm:ss")
    // パラメータ
    var param = {
        "user_id" : $("#user_id").val()
        , "room_no" : $("#room_no").val()
        , "message" : $("#message").val()
        , "send_date" : send_date
    }

    socket.send(JSON.stringify(param));
}

/**
 * チャット相手選択イベント.
 */
function room_select(room_no, user_id) {

	var param = {
		room_no : room_no
		, user_id : user_id
	}

	$.ajax({
		url: "http://localhost:8888/select",
		type: "POST",
        headers: {'X-XSRFToken' : $("*[name=_xsrf]")[0].value },
		data: JSON.stringify(param),
		success: function(jsonResponse) {
			jsonResponse = jsonResponse.replace( /\\/g , "" );
			var data = JSON.parse(jsonResponse);

            // 部屋番号の書き換え
            $("#room_no").val(data.room_no);

			// コンタクトリストの書き換え
			$("#user_" + data.user_id + " a").attr("href", "javascript:room_select(" + data.room_no + ", " + data.user_id + ");");

			// メッセージの表示
		    $(".direct-chat-messages").empty();
			for(row in data.messages) {
			    $(".direct-chat-messages").append(createMessage(data.messages[row]));
			}
            $(".direct-chat-messages").animate({
                scrollTop: $(".direct-chat-messages")[0].scrollHeight
            }, 0);
		    $("#chat-panel").removeClass("direct-chat-contacts-open");

		    $("#sendButton").addClass("btn-warning");
		    $("#sendButton").removeClass("btn-disable");
		    $("#sendButton").prop("disabled", false);
		},
		error: function() {
		}
	});
}

 

起動してみる

初期表示
コンタクトリスト
相手選択後

まとめ

とりあえず、配信部分もトークルームの番号で振り分けるようなイメージで進んでいます。
あと数回でチャット編は終わりにしようと思いますが、
この後は未読の仕組みとグループトークの仕組みをいれていければと思います。

ではでは。

 

スポンサーリンク


関連するコンテンツ

2018年10月13日Python,開発AdminLTE,CSSフレームワーク,Python,Tornado,WebSocket,プログラミング

Posted by doradora