【Python】AdminLTEとWebSocketでチャット機能を作ってみる その2(ログイン機能)
おはようございます。
チャットの実装ですが、
ユーザー毎の制御(トークルームみたいな)を実現するために
MySQLにマスタを持たせて、ひとまずログイン機能を実装します。
プログラムは前回のものを流用します。
【Python】AdminLTEとWebSocketでチャット機能を作ってみる その1(とりあえず版)
スポンサーリンク
下準備
今回から、MySQLを使っていきますのでその下準備を。
次のテーブルを作成してデータを登録しておきます。
テーブル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | -- パスワードマスタ createtableMST_PASSWORD( USER_IDvarchar(20)not nullcomment'ユーザーID' ,PASSWORDvarchar(128)not nullcomment'パスワード' ,CREATE_USERvarchar(20)comment'作成者' ,CREATE_DATEdatetimecomment'作成日時' ,UPDATE_USERvarchar(20)comment'更新者' ,UPDATE_DATEdatetimecomment'更新日時' ,constraintMST_PASSWORD_PKCprimary key(USER_ID) )comment'パスワードマスタ'; -- ユーザマスタ createtableMST_USER( USER_IDvarchar(20)not nullcomment'ユーザーID' ,USER_NAMEvarchar(60)not nullcomment'ユーザー名' ,ICONvarchar(20)comment'アイコン:画像ファイル名' ,MESSAGEvarchar(50)comment'一言メッセージ' ,CREATE_USERvarchar(20)comment'作成者' ,CREATE_DATEdatetimecomment'作成日時' ,UPDATE_USERvarchar(20)comment'更新者' ,UPDATE_DATEdatetimecomment'更新日時' ,constraintMST_USER_PKCprimary key(USER_ID) )comment'ユーザマスタ'; |
データ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | -- MSTユーザー DELETEFROMMST_USER; INSERTINTOMST_USERVALUES('001','そら','sora.jpg','いいことないかな','INIT',NULL,NULL,NULL); INSERTINTOMST_USERVALUES('002','りく','riku.jpg','ちゅーるちゅーるちゃおちゅーるー','INIT',NULL,NULL,NULL); INSERTINTOMST_USERVALUES('003','うみ','umi.jpg','誰かブラッシングしてくれないかしら','INIT',NULL,NULL,NULL); INSERTINTOMST_USERVALUES('004','こうめ','koume.jpg','ごはんまだ?','INIT',NULL,NULL,NULL); INSERTINTOMST_USERVALUES('005','こなつ','konatsu.jpg','早く新しい家に引っ越ししたい','INIT',NULL,NULL,NULL); -- MSTパスワード DELETEFROMMST_PASSWORD INSERTINTOMST_PASSWORDVALUES('001','001','INIT',NULL,NULL,NULL); INSERTINTOMST_PASSWORDVALUES('002','002','INIT',NULL,NULL,NULL); INSERTINTOMST_PASSWORDVALUES('003','003','INIT',NULL,NULL,NULL); INSERTINTOMST_PASSWORDVALUES('004','004','INIT',NULL,NULL,NULL); INSERTINTOMST_PASSWORDVALUES('005','005','INIT',NULL,NULL,NULL); |
ライブラリの追加
次の2つを、「メニュー」>「Default Settings」よりインストールします。
- mysql-connector-python-rf
- configparser
フォルダ構成
フォルダ構成は次のようになります。
前回からの変更点は、conf、Daoあたりですかね。
SampleChat
│ Main.py
│
├─conf
│ db.config
│
├─Dao
│ MstPasswordDao.py
│ MstUserDao.py
│ SqlClient.py
│
├─static
│ ├─css
│ │ │ AdminLTE.css
│ │ │ AdminLTE.min.css
│ │ │ login.css
│ │ │ style.css
│ │ │
│ │ └─skins
│ │ skin-blue.css
│ │ skin-blue.min.css
│ │
│ ├─img
│ │ konatsu.jpg
│ │ koume.jpg
│ │ riku.jpg
│ │ sora.jpg
│ │ umi.jpg
│ │
│ └─js
│ adminlte.min.js
│ script.js
│
└─templates
login.html
main.html
画面
ログイン画面
以前記事に書いた【Python】FullCalendarにログイン機能をつけてみる を流用しました。
画面
login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>チャットサンプル- ログイン</title> <link rel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet"href="{{ static_url('css/login.css') }}"/> <link rel="stylesheet"href="https:////maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <script type="text/javascript"src="http://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <script type="text/javascript"src="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script type="text/javascript"> functionsubmitLogin(){ $("#loginForm").submit(); } </script> </head> <body> <form id="loginForm"method="post"action="/login"> <div class="jumbotron"> <div class="container"> <span class="fa fa-comments"></span> <h2>チャットサンプル</h2> <div class="box"> {% module xsrf_form_html() %} <input id="inputUserId"name="user_id"type="text"placeholder="ユーザーID"> <input id="inputPassword"name="password"type="password"placeholder="パスワード"> <span class="errMsg">{{ error_msg }}</span> <button class="btn btn-default full-width"onclick="submitLogin()"> <span class="glyphicon glyphicon-ok"></span> </button> </div> </div> </div> </form> </body> </html> |
スタイル
login.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | body { background:#ffbb55nonerepeatscroll00; } .jumbotron { text-align:center; width:35rem; border-radius:0.5rem; top:0; bottom:0; left:0; right:0; position:absolute; margin:4remauto; background-color:#fff; padding:2rem; height:45rem; } .container .fa { font-size:10rem; margin-top:3rem; color:#f96145; } input { width:100%; margin-bottom:1.4rem; padding:1rem; background-color:#ecf2f4; border-radius:0.2rem; border:none; } h2 { margin-bottom:3rem; font-weight:bold; color:#ababab; } .btn { border-radius:0.2rem; } .btn .glyphicon { font-size:3rem; color:#fff; } .full-width { background-color:#8eb5e2; width:100%; -webkit-border-top-right-radius:0; -webkit-border-bottom-right-radius:0; -moz-border-radius-topright:0; -moz-border-radius-bottomright:0; border-top-right-radius:0; border-bottom-right-radius:0; } .box { position:absolute; bottom:0; left:0; margin-bottom:3rem; margin-left:3rem; margin-right:3rem; } span.errMsg { color:#f96145; font-size:11px; } |
メイン画面
ヘッダーメニューを追加、
OSSのCSS、JSはCDNから取得するように変更しました。
main.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | <!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"> <!-- 2.ヘッダ情報 --> <div class="navbar-header"style="padding:15px;"> ユーザー名:{{ user_name }} </div> <!-- 3.リストの配置 --> <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> <!-- 4.ボタン --> <button type="button"class="pull-right btn btn-default navbar-btn"onclick="logout();"> ログアウト <span class="fa fa-sign-out"></span> </button> </div> </nav> <section class="content container-fluid"> <div class="row"> <!-- Left col --> <div class="col-xs-8"> <!-- /.box --> <div class="row"> <div class="col-xs-8"> <!-- DIRECT CHAT --> <div id="chat-panel"class="box box-warning direct-chat direct-chat-warning box-solid"style="display:none;"> <div class="box-header with-border"> <h3 class="box-title">チャットメッセージ</h3> <div class="box-tools pull-right"> <span id="status"class="status"></span> <span data-toggle="tooltip"title="3 New Messages"class="badge bg-yellow">3</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> <!-- /.box-header --> <div class="box-body"> <!-- Conversations are loaded here --> <div class="direct-chat-messages"> <!-- Message. Default to the left --> <div class="direct-chat-msg"> <div class="direct-chat-info clearfix"> <span class="direct-chat-name pull-left">こなつ</span> <span class="direct-chat-timestamp pull-right">2018/09/25(月) 02:00</span> </div> <!-- /.direct-chat-info --> <img class="direct-chat-img"src="static/img/konatsu.jpg"alt="message user image"> <!-- /.direct-chat-img --> <div class="direct-chat-text"> そら最近どうしてる? </div> <!-- /.direct-chat-text --> </div> <!-- /.direct-chat-msg --> <!-- Message to the right --> <div class="direct-chat-msg right"> <div class="direct-chat-info clearfix"> <span class="direct-chat-name pull-right">そら</span> <span class="direct-chat-timestamp pull-left">2018/09/25(月) 02:05</span> </div> <!-- /.direct-chat-info --> <img class="direct-chat-img"src="static/img/sora.jpg"alt="message user image"> <!-- /.direct-chat-img --> <div class="direct-chat-text"> 相変わらずだよ。<BR> あいつらの面倒で手一杯でさ。 </div> <!-- /.direct-chat-text --> </div> <!-- /.direct-chat-msg --> <!-- Message. Default to the left --> <div class="direct-chat-msg"> <div class="direct-chat-info clearfix"> <span class="direct-chat-name pull-left">こなつ</span> <span class="direct-chat-timestamp pull-right">2018/09/25(月) 05:37</span> </div> <!-- /.direct-chat-info --> <img class="direct-chat-img"src="static/img/konatsu.jpg"alt="message user image"> <!-- /.direct-chat-img --> <div class="direct-chat-text"> 一番のお兄さんだから大変ね。<BR> 私は一人で快適な暮らしを送っているわ(^^♪ </div> <!-- /.direct-chat-text --> </div> <!-- /.direct-chat-msg --> <!-- Message to the right --> <div class="direct-chat-msg right"> <div class="direct-chat-info clearfix"> <span class="direct-chat-name pull-right">そら</span> <span class="direct-chat-timestamp pull-left">2018/09/25(月) 06:10</span> </div> <!-- /.direct-chat-info --> <img class="direct-chat-img"src="static/img/sora.jpg"alt="message user image"> <!-- /.direct-chat-img --> <div class="direct-chat-text"> え、なにそれ自慢ですか? </div> <!-- /.direct-chat-text --> </div> <!-- /.direct-chat-msg --> </div> <!--/.direct-chat-messages--> <!-- Contacts are loaded here --> <div class="direct-chat-contacts"> <ul class="contacts-list"> <li> <a href="#"> <img class="contacts-list-img"src="static/img/konatsu.jpg"alt="User Image"> <div class="contacts-list-info"> <span class="contacts-list-name"> こなつ <small class="contacts-list-date pull-right">2018/09/25(月)</small> </span> <span class="contacts-list-msg">早く新しい家に引っ越ししたい。</span> </div> <!-- /.contacts-list-info --> </a> </li> <li> <a href="#"> <img class="contacts-list-img"src="static/img/umi.jpg"alt="User Image"> <div class="contacts-list-info"> <span class="contacts-list-name"> うみ <small class="contacts-list-date pull-right">2018/09/25(月)</small> </span> <span class="contacts-list-msg">誰かブラッシングしてくれないかしら。</span> </div> <!-- /.contacts-list-info --> </a> </li> <li> <a href="#"> <img class="contacts-list-img"src="static/img/koume.jpg"alt="User Image"> <div class="contacts-list-info"> <span class="contacts-list-name"> こうめ <small class="contacts-list-date pull-right">2018/09/24(日)</small> </span> <span class="contacts-list-msg">ちゅーるちゅーるちゃおちゅーるー</span> </div> <!-- /.contacts-list-info --> </a> </li> <li> <a href="#"> <img class="contacts-list-img"src="static/img/riku.jpg"alt="User Image"> <div class="contacts-list-info"> <span class="contacts-list-name"> りく <small class="contacts-list-date pull-right">2018/09/12(水)</small> </span> <span class="contacts-list-msg">ごはんまだ?</span> </div> <!-- /.contacts-list-info --> </a> </li> <!-- End Contact Item --> </ul> <!-- /.contatcts-list --> </div> <!-- /.direct-chat-pane --> </div> <!-- /.box-body --> <div class="box-footer"> <form action="#"method="post"> <div class="input-group"> <input id="message"type="text"name="message"placeholder="Type Message ..."class="form-control"> <span class="input-group-btn"> <button id="sendButton"type="button"class="btn btn-warning btn-flat">Send</button> </span> </div> </form> </div> <!-- /.box-footer--> </div> <!--/.direct-chat --> </div> <!-- /.col --> </div> <!-- /.col --> </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> |
プログラム
新規追加
MySQLとやり取りするクラスを新規で作成します。
Dao/SqlClient.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | importmysql.connector fromcontextlibimportclosing importconfigparser classSqlClient: """ SQLクライアントクラス. """ def__init__(self,host=None,port=None,user=None,password=None,database=None): """ イニシャライザ :param host: :param port: :param user: :param password: :param database: """ conf=configparser.ConfigParser() conf.read("conf/db.config") # 指定されていないパラメータは設定ファイルから読み込み host=host ifhost isnotNoneelseconf.get("MySQL","host") port=port ifport isnotNoneelseconf.get("MySQL","port") user=userifuserisnotNoneelseconf.get("MySQL","user") password=password ifpassword isnotNoneelseconf.get("MySQL","password") database=database ifdatabase isnotNoneelseconf.get("MySQL","database") self.config={ "host":host, "port":port, "user":user, "password":password, "database":database } defselect(self,sql,data=None): """ データを検索します :param sql: :param data: :return: """ withclosing(mysql.connector.connect(**self.config))asconn: c=conn.cursor(dictionary=True) c.execute(sql,data) returnc.fetchall() defexecute(self,sql,data): """ クエリを実行します. :param sql: :param data: :return: """ withclosing(mysql.connector.connect(**self.config))asconn: c=conn.cursor() c.execute(sql,data) c.close() conn.commit() |
さらに、それぞれのテーブルよりデータを取得するためのクラスを追加。
Dao/MstUserDao.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | fromDao.SqlClient importSqlClient classMstUserDao(SqlClient): """ MSTユーザーDAOクラス """ defselect_user(self,user_id): """ ユーザIDを指定してデータを取得します :param user_id: :return: """ sql="SELECT * FROM MST_USER WHERE USER_ID = %s" data=super().select(sql,[user_id]) iflen(data)>0: returnself.mapping_data(data[0]) returnNone @staticmethod defmapping_data(record): """ レコードをマッピングします :param record: :return: """ dic={ "user_id":record['USER_ID'], "user_name":record['USER_NAME'], "icon":record['ICON'], "message":record['MESSAGE'], "createUser":record['CREATE_USER'], "createDate":record['CREATE_DATE'], "updateUser":record['UPDATE_USER'], "updateDate":record['UPDATE_DATE']} returndic |
MstPasswordDao.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | fromDao.SqlClient importSqlClient classMstPasswordDao(SqlClient): """ MSTパスワードDAOクラス """ defselect_password(self,user_id): """ ユーザIDを指定してパスワードを取得します :param user_id: :return: """ sql="SELECT * FROM MST_PASSWORD WHERE USER_ID = %s" data=super().select(sql,[user_id]) iflen(data)>0: dic=self.mapping_data(data[0]) returndic['password'] return"" @staticmethod defmapping_data(record): """ レコードをマッピングします :param record: :return: """ dic={ "user_id":record['USER_ID'], "password":record['PASSWORD'], "createUser":record['CREATE_USER'], "createDate":record['CREATE_DATE'], "updateUser":record['UPDATE_USER'], "updateDate":record['UPDATE_DATE']} returndic |
既存クラスの修正
メイン処理にログイン機能を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | # --- coding: utf-8 --- """ チャットサンプル """ importos importlogging importtornado.web importtornado.ioloop importtornado.websocket fromtornado.web importRequestHandler fromtornado.options importoptions fromtornado.websocket importWebSocketHandler fromDao.MstPasswordDao importMstPasswordDao fromDao.MstUserDao importMstUserDao client=[] classAuthBaseHandler(RequestHandler): """ 認証ハンドラー基底クラス """ cookie_user_id="user_id" defget_current_user(self): logging.info("AuthBaseHandler [get_current_user]") user_id=self.get_secure_cookie(self.cookie_user_id) ifnotuser_id: return"" returnuser_id.decode("UTF-8") defset_current_user(self,user_id): logging.info("AuthBaseHandler [set_current_user]") self.set_secure_cookie(self.cookie_user_id,user_id) defclear_current_user(self): logging.info("AuthBaseHandler [clear_current_user]") self.clear_cookie(self.cookie_user_id) classAuthLoginHandler(AuthBaseHandler): """ ログインハンドラー """ defget(self): logging.info("AuthLoginHandler [get]") self.render("Login.html",error_msg="") defpost(self): logging.info("AuthLoginHandler [post]") self.check_xsrf_cookie() # 認証処理 input_user_id=self.get_argument("user_id") input_password=self.get_argument("password") dao=MstPasswordDao() password=dao.select_password(input_user_id) # 入力されたパスワードと保存されているパスワードをチェック is_auth=Falseifinput_password!=password elseTrue ifis_auth: self.set_current_user(input_user_id) self.redirect("/main") else: self.render("login.html",error_msg="ユーザーコードまたはパスワードが間違っています。") classAuthLogoutHandler(AuthBaseHandler): """ ログアウトハンドラー """ defget(self): self.clear_current_user() self.redirect('/login') classMainHandler(AuthBaseHandler): """ 初期表示処理 """ definitialize(self): logging.info("[MainHandler] initialize") @tornado.web.authenticated defget(self): logging.info("[MainHandler] get") dao=MstUserDao() user=dao.select_user(self.get_current_user()) user_name=user['user_name'] self.render("main.html",user_name=user_name) classChatHandler(WebSocketHandler): """ チャット処理 """ defopen(self): logging.info("[ChatHandler] open") ifselfnotinclient: client.append(self) defon_message(self,message): logging.info("[ChatHandler] on_message : "+message) forcl inclient: cl.write_message(message) defon_close(self): logging.info("[ChatHandler] on_close") ifselfinclient: client.remove(self) application=tornado.web.Application([ (r"/login",AuthLoginHandler), (r"/logout",AuthLogoutHandler), (r"/main",MainHandler), (r"/chat",ChatHandler), ], 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 ) if__name__=="__main__": tornado.options.parse_command_line() application.listen(8888) logging.info("server started") tornado.ioloop.IOLoop.instance().start() |
起動してみる
ユーザID、パスワードを入力してボタンをクリック。
チャット画面が表示されました。
まとめ
ログイン自体は以前もやったので大したことはなかったのですが、トークルームの実装について悩んでいます。
(複数人でのチャットや入室、退室をどうしよう)
とりあえず次回はデータを取得して表示するのと、
メッセージ送信時にDBに登録するところをやってみようかと思います。
ではでは。
ディスカッション
コメント一覧
まだ、コメントがありません