【Python】FullCalendarとBootstrapで詳細な予定を登録できるようにする
おはようございます。
前回に引き続き、
FullCalendarとMySQLとの連携ですが
今回は Bootstrap を使って入力ダイアログを表示し、少し詳細な予定を登録できるようにしていきます。
プログラムは前回のを流用します。
スポンサーリンク
データベース変更
新たに、予定の詳細用にカラムを追加します。
1 2 3 | ALTERTABLETBL_SCHEDULEADDCOLUMN( DESCRIPTIONVARCHAR(1000) ) |
画面の修正
変更点
1. 予定の入力フォームを Bootstrapのダイアログで作成
2. 日付、時刻の入力にBootstrap DateTimePicker、moment.jsを利用
3. Javascriptを外部ファイルにする
Bootstrap、DateTimePicker用のライブラリをCDNから読み込み、
(普段は非表示の)予定入力モーダルダイアログを追加。
index.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 | <!DOCTYPE html> <html> <head> <title>カレンダーサンプル</title> <link rel="stylesheet"href="{{ static_url('css/fullcalendar.min.css') }}"/> <link rel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet"href="{{ static_url('css/style.css') }}"/> <link rel="stylesheet"href="http://cdn.rawgit.com/Eonasdan/bootstrap-datetimepicker/v4.0.0/build/css/bootstrap-datetimepicker.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 src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment-with-locales.js"></script> <script type="text/javascript"src="{{ static_url('js/moment.min.js') }}"></script> <script type="text/javascript"src="{{ static_url('js/fullcalendar.min.js') }}"></script> <script type="text/javascript"src="{{ static_url('lang/ja.js') }}"></script> <script src="http://cdn.rawgit.com/Eonasdan/bootstrap-datetimepicker/v4.0.0/src/js/bootstrap-datetimepicker.js"></script> <script type="text/javascript"src="{{ static_url('js/script.js') }}"></script> <script> // ページ読み込み時の処理 $(document).ready(function(){ initializePage(); }); </script> </head> <body> <div id='calendar'></div> <div id="inputScheduleForm"class="modal fade"tabindex="-1"> <div class="modal-dialog modal-nm"> <div class="modal-content"> <div class="modal-header"> <button type="button"class="close"data-dismiss="modal"aria-hidden="true">×</button> <h4 class="modal-title">スケジュール登録</h4> </div> <div class="modal-body"> <div class="container"> <div class="row"> <div class="col-md-2"> <label for="">タイトル</label> </div> <div class="col-md-5"> <input id="inputTitle"type="text"class="form-control input-sm ime-active"placeholder="タイトル"value="" > </div> </div> <div class="row"> <div class="col-md-2 required"> <label for="">日時</label> </div> <div class="col-md-5"> <div class="input-group"> <div class="checkbox"style="margin-top: 0px;"> <input type="checkbox"id="allDayCheck"onclick="allDayCheckClick(this);"checked/><label for="allDayCheck">終日</label> </div> </div> <div class="form-inline"> <div class="form-group"style="position:relative;"> <input id="inputYmdFrom"type="text"class="form-control input-sm ymd"value=""/> <input id="inputYmdHmFrom"type="text"class="form-control input-sm ymdHm"/> ~ <input id="inputYmdTo"type="text"class="form-control input-sm ymd" /> <input id="inputYmdHmTo"type="text"class="form-control input-sm ymdHm" /> </div> </div> </div> </div> <div class="row"> <div class="col-md-2"> <label for="">詳細</label> </div> <div class="col-md-5"> <textarea id="inputDescription"class="form-control ime-active"rows="5"placeholder="詳細"></textarea> </div> </div> </div> </div> <div class="modal-footer"> <div id="inputError"class="pull-left"style="color:red; padding:5px;"></div> <button type="button"class="btn btn-primary"onclick="registSchedule();">登録</button> <button type="button"class="btn btn-default"data-dismiss="modal">閉じる</button> </div> </div> </div> </div> </body> </html> |
入力フォーム用にスタイル定義を追加。
style.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 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 | /** * Bootstrapカスタマイズ */ /* テキストIME */ input { ime-mode:disabled; } input.ime-active { ime-mode:active; } input.ime-inactive { ime-mode:inactive; } /* モーダルダイアログ */ .panel-body > .container > .row, .modal-body > .container > .row { margin:10px; } .container > .row > div.required label:after { content:"*";color:red; } .form-inline > .form-group > input.ymd { width:110px; } .form-inline > .form-group > input.ymdHm { width:160px; } .hidden { visibility:hidden; display:none; } /* チェックボックス */ [type="checkbox"]:checked, [type="checkbox"]:not(:checked) { position:absolute; left:-9999px; } [type="checkbox"]:checked + label, [type="checkbox"]:not(:checked) + label { font-weight:normal; position:relative; padding-left:24px; cursor:pointer; line-height:18px; display:inline-block; color:#666; } [type="checkbox"]:checked + label:before, [type="checkbox"]:not(:checked) + label:before { content:''; position:absolute; left:0; top:0; width:20px; height:20px; border:1pxsolid#ddd; background:#fff; } [type="checkbox"]:focus + label:before, [type="checkbox"]:active + label:before{ content:''; position:absolute; width:20px; height:20px; left:0; top:0; border:1pxsolidrgba(128,128,128,1); box-shadow:0px1px1pxrgba(0,0,0,0.075)inset,0px0px8pxrgba(128,128,128,1); background:#fff; } [type="checkbox"]:checked + label:after, [type="checkbox"]:not(:checked) + label:after { content:''; width:8px; height:12px; background:#ffffff; border-right:3pxsolid#808080; border-bottom:3pxsolid#808080; position:absolute; top:2px; left:6px; -webkit-transition:all0.2sease; transition:all0.2sease; } [type="checkbox"]:not(:checked) + label:after { opacity:0; -webkit-transform:rotate(45deg); -ms-transform:rotate(45deg); transform:rotate(45deg); } [type="checkbox"]:checked + label:after { opacity:1; -webkit-transform:rotate(45deg); -ms-transform:rotate(45deg); transform:rotate(45deg); } |
プログラムの修正
クライアント側プログラム(JavaScript)
ちょっと日付まわりとか色々とごにょごにょしていますが、そのうち綺麗に整理します。
script.js
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 | /** * ページ初期処理. */ functioninitializePage(){ // カレンダーの設定 $('#calendar').fullCalendar({ height:550, lang:"ja", header:{ left:'prev,next today', center:'title', right:'month,basicWeek,basicDay' }, timeFormat:'HH:mm', selectable:true, selectHelper:true, navLinks:true, eventSources:[{ url:'http://localhost:8080/getCalendar', dataType:'json', async:false, type:'GET', error:function(){ $('#script-warning').show(); } }], select:function(start,end,resource){ // 日付選択された際のイベント // タイトル初期化 $("#inputTitle").val(""); $('#inputScheduleForm').on('show.bs.modal',function(event){ setTimeout(function(){ $('#inputTitle').focus(); },500); }).modal("show"); // 日付ピッカーの設定 $('.ymdHm').hide() $('#inputYmdFrom').datetimepicker({locale:'ja',format:'YYYY年MM月DD日',useCurrent:false}); $('#inputYmdTo').datetimepicker({locale:'ja',format:'YYYY年MM月DD日',useCurrent:false}); $('.ymdHm').datetimepicker({ locale:'ja', format:'YYYY年MM月DD日 HH時mm分' }); // 開始終了が逆転しないように制御 $("#inputYmdFrom").on("dp.change",function(e){ $('#inputYmdTo').data("DateTimePicker").minDate(e.date); }); $("#inputYmdTo").on("dp.change",function(e){ $('#inputYmdFrom').data("DateTimePicker").maxDate(e.date); }); // 終日チェックボックス $('#allDayCheck').prop("checked",true); // 選択された日付をフォームにセット // FullCalendar の仕様で、終了が翌日の00:00になるため小細工 varstartYmd=moment(start); varendYmd=moment(end); if(endYmd.diff(startYmd,'days')>1){ endYmd=endYmd.add(-1,"days"); }else{ endYmd=startYmd; } $('#inputYmdFrom').val(startYmd.format("YYYY年MM月DD日")); $('#inputYmdFrom').data("DateTimePicker").date(startYmd.format("YYYY年MM月DD日")); $('#inputYmdTo').val(endYmd.format("YYYY年MM月DD日")); $('#inputYmdTo').data("DateTimePicker").date(endYmd.format("YYYY年MM月DD日")); }, eventClick:function(event){ // 予定クリック時のイベント console.log(event); }, editable:true, eventLimit:true }); } /** * 予定入力フォームの登録ボタンクリックイベント. */ functionregistSchedule(){ varstartYmd=moment(formatNengappi($('#inputYmdFrom').val()+"00時00分00",1)); varendYmd=moment(formatNengappi($('#inputYmdTo').val()+"00時00分00",1)); varallDayCheck=$('#allDayCheck').prop("checked"); if(!allDayCheck){ startYmd=moment(formatNengappi($('#inputYmdHmFrom').val()+"00",1)); endYmd=moment(formatNengappi($('#inputYmdHmTo').val()+"00",1)); } if(endYmd.diff(startYmd,'days')>0){ endYmd=endYmd.add(+1,"days"); } vareventData; if($('#inputTitle').val()){ eventData={ title:$('#inputTitle').val(), start:startYmd.format("YYYY-MM-DDTHH:mm:ss"), end:endYmd.format("YYYY-MM-DDTHH:mm:ss"), allDay:allDayCheck, description:$('#inputDescription').val() }; $.ajax({ url:"http://localhost:8080/regist", type:"POST", data:JSON.stringify(eventData), success:function(jsonResponse){ $('#calendar').fullCalendar('renderEvent',jsonResponse,true); alert("予定を登録しました。"); }, error:function(){ } }); } $('#calendar').fullCalendar('unselect'); } /** * 終日チェックボックスクリックイベント. * */ functionallDayCheckClick(element){ if(element&& element.checked) { $('.ymdHm').hide(); $('.ymd').show(); } else{ varstartYmd=moment(formatNengappi($("#inputYmdFrom").val(),0)); varendYmd=moment(formatNengappi($("#inputYmdTo").val(),0)); varstartYmdHm=moment(startYmd.format("YYYY-MM-DD")+"T"+moment().format("HH")+":00:00"); varendYmdHm=moment(startYmd.format("YYYY-MM-DD")+"T"+moment().format("HH")+":00:00").add(1,"hours"); $("#inputYmdHmFrom").val(startYmdHm.format("YYYY年MM月DD日 HH時mm分")); $("#inputYmdHmTo").val(endYmdHm.format("YYYY年MM月DD日 HH時mm分")); $('.ymd').hide(); $('.ymdHm').show(); } } /** * 年月日の形式を変換する. */ functionformatNengappi(nengappi,flg){ varret=nengappi.replace("年","-").replace("月","-").replace("日",""); if(flg==1){ ret=nengappi.replace("年","-").replace("月","-").replace("日","T").replace("時",":").replace("分",":").replace(" ",""); } returnret; } |
サーバー側(Python)
追加したカラムもクライアントから連携して登録するように修正。
また、クライアントにJSONを返すようにして、画面の情報も更新します。
Main.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 | classRegistSchedule(RequestHandler): """ スケジュール登録 """ definitialize(self): logging.info("RegistSchedule [initialize]") defpost(self): logging.info("RegistSchedule [post]") mysql=MySQLUtil() param=json.loads(self.request.body) allday="TRUE"ifparam["allDay"]else"FALSE" id=mysql.get_next_id('001') data=[ "001", id, param["title"], param["start"], param["end"], "", "", "", allday, param["description"] ] mysql.insert_data(data) # IDを設定して返す param["id"]=id self.write(param) |
MySQLUtil.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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | defcreate_db(self): """ データベース、及び必要なテーブルを作成します. :return: """ withclosing(mysql.connector.connect(**self.config))asconn: c=conn.cursor() # スケジュールテーブル sql="CREATE TABLE IF NOT EXISTS TBL_SCHEDULE (" sql+=" USER_CD VARCHAR(10)" sql+=", ID INT(10)" sql+=", TITLE VARCHAR(100)" sql+=", START DATETIME" sql+=", END DATETIME" sql+=", TEXTCOLOR VARCHAR(20)" sql+=", COLOR VARCHAR(20)" sql+=", URL VARCHAR(100)" sql+=", ALLDAY VARCHAR(10)" sql+=", DESCRIPTION VARCHAR(1000)" sql+=", PRIMARY KEY(USER_CD, ID)" sql+=")" c.execute(sql) c.close() conn.commit() definsert_data(self,data): """ データを登録します :param ticker: :return: """ withclosing(mysql.connector.connect(**self.config))asconn: c=conn.cursor() # データ登録 sql="INSERT INTO TBL_SCHEDULE VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)" c.execute(sql,data) c.close() conn.commit() defget_schedule(self,year_month,user_cd=""): """ データ(テーブル)をデータフレームに変換してかえします :return: """ result=[] withclosing(mysql.connector.connect(**self.config))asconn: c=conn.cursor(dictionary=True) sql="SELECT * FROM TBL_SCHEDULE" sql+=" WHERE (DATE_FORMAT(START, '%Y%m') = '"+year_month+"'" sql+=" OR DATE_FORMAT(END, '%Y%m') = '"+year_month+"')" ifuser_cd!="": sql+=" AND USER = '"+user_cd+"'" sql+=" ORDER BY USER_CD, ID" c.execute(sql) forrinc.fetchall(): result.append({ "user_cd":r['USER_CD'], "id":r['ID'], "title":r['TITLE'], "start":r['START'], "end":r['END'], "textColor":r['TEXTCOLOR'], "color":r['COLOR'], "url":r['URL'], "allDay":(r['ALLDAY']=='TRUE'), "description":r['DESCRIPTION'] }) returnresult |
起動してみる
日付をクリック、またはドラッグすると予定入力用のダイアログフォームが表示されます。
予定を入力して登録ボタンをクリックします。
登録完了メッセージが表示されればOKです。
ダイアログを閉じると、カレンダーに登録された予定が反映されています。
まとめ
細かいところは全然なので最終的にブラッシュアップが必要ですが、とりあえず使える感じにはなってきてますね。
少し長くなってしまったので、
既に登録されている予定の更新や削除はまた次回に。
ではでは。
ディスカッション
コメント一覧
初めまして!
まさに求めていた機能でしたので、参考にさせていただいております。
イベント登録時に、タイトル、日にちは登録できるのですが時間が登録できません。
コンソールで見ると途中まで時間が渡されていたんですが、eventDataに入れるときにInvalid Dataになっておりました。
もし解決策がお分かりでしたらご教示いただければとおもいます。
function registSchedule() {
var startYmd = moment(formatNengappi($(‘#inputYmdFrom’).val() + “00時00分00”, 1));
var endYmd = moment(formatNengappi($(‘#inputYmdTo’).val() + “00時00分00”, 1));
var allDayCheck = $(‘#allDayCheck’).prop(“checked”);
if (!allDayCheck) {
startYmd = moment(formatNengappi($(‘#inputYmdHmFrom’).val(), 1));
endYmd = moment(formatNengappi($(‘#inputYmdHmTo’).val(), 1));
}
if (endYmd.diff(startYmd, ‘days’) > 0) {
endYmd = endYmd.add(+1, “days”);
}
var eventData;
if ($(‘#inputTitle’).val()) {
eventData = {
title: $(‘#inputTitle’).val(),
start: startYmd.format(“YYYY-MM-DDTHH:mm:ss”),
end: endYmd.format(“YYYY-MM-DDTHH:mm:ss”),
allDay: allDayCheck,
description: $(‘#inputDescription’).val()
};
$.ajax({
url: “http://localhost:8080/regist”,
type: “POST”,
data: JSON.stringify(eventData),
success: function(jsonResponse) {
$(‘#calendar’).fullCalendar(‘renderEvent’, jsonResponse, true);
alert(“予定を登録しました。”);
},
error: function() {
}
});
}
$(‘#calendar’).fullCalendar(‘unselect’);
}
koerisu 様
いつもブログを観ていただきありがとうございます。
ご質問の件、確認しますので少々お待ちいただけますか。
スミマセンちょっと余裕がなくって。。
doradora様
ご返信ありがとうございます。
はい!もちろん大丈夫です。
お手数おかけします。
koerisu 様
ご連絡遅くなりすみません。
次の部分を修正すれば大丈夫かと思います!
if (!allDayCheck) {
//startYmd = moment(formatNengappi($(‘#inputYmdHmFrom’).val(), 1));
//endYmd = moment(formatNengappi($(‘#inputYmdHmTo’).val(), 1));
startYmd = moment(formatNengappi($(‘#inputYmdHmFrom’).val() + “00”, 1));
endYmd = moment(formatNengappi($(‘#inputYmdHmTo’).val() + “00”, 1));
}
よろしくお願いします。
ご連絡が遅くなり申し訳ございません
ありがとうございます。無事解決することができました!
ただ、一難去ってまた一難なのですが、登録したスケジュールが登録日よりも1日前に登録されてしまう状態になっておりました・・・
例えば4/15のスケジュールを登録したら4/14にスケジュールが表示されてしまいます
DBでは4/15にちゃんと登録がなっているのですが・・・もし同じ事案があればご教示いただきたいです!
お忙しいところすみません。よろしくお願いいたします。
koerisu 様
ひとまず解決できたとのことで、よかったです。
前日が表示されてしまう件、こちらで再現できませんでした。
下記までソースをお送りいただければ私の方でも調査してみます。
doraxdora.gm.biz@gmail.com
よろしくお願い致します。