【Python】FullCalendarとBootstrapで詳細な予定を登録できるようにする
おはようございます。
前回に引き続き、
FullCalendarとMySQLとの連携ですが
今回は Bootstrap を使って入力ダイアログを表示し、少し詳細な予定を登録できるようにしていきます。
プログラムは前回のを流用します。
スポンサーリンク
データベース変更
新たに、予定の詳細用にカラムを追加します。
ALTER TABLE TBL_SCHEDULE ADD COLUMN ( DESCRIPTION VARCHAR(1000) )
画面の修正
変更点
1. 予定の入力フォームを Bootstrapのダイアログで作成
2. 日付、時刻の入力にBootstrap DateTimePicker、moment.jsを利用
3. Javascriptを外部ファイルにする
Bootstrap、DateTimePicker用のライブラリをCDNから読み込み、
(普段は非表示の)予定入力モーダルダイアログを追加。
index.html
<!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" 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">登録</button> <button type="button" class="btn btn-default" data-dismiss="modal">閉じる</button> </div> </div> </div> </div> </body> </html>
入力フォーム用にスタイル定義を追加。
style.css
/** * 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: 1px solid #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: 1px solid rgba(128, 128, 128, 1); box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px rgba(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: 3px solid #808080; border-bottom: 3px solid #808080; position: absolute; top: 2px; left: 6px; -webkit-transition: all 0.2s ease; transition: all 0.2s ease; } [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
/** * ページ初期処理. */ function initializePage() { // カレンダーの設定 $('#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になるため小細工 var startYmd = moment(start); var endYmd = 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 }); } /** * 予定入力フォームの登録ボタンクリックイベント. */ 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() + "00", 1)); endYmd = moment(formatNengappi($('#inputYmdHmTo').val() + "00", 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'); } /** * 終日チェックボックスクリックイベント. * */ function allDayCheckClick(element) { if (element && element.checked) { $('.ymdHm').hide(); $('.ymd').show(); } else { var startYmd = moment(formatNengappi($("#inputYmdFrom").val(), 0)); var endYmd = moment(formatNengappi($("#inputYmdTo").val(), 0)); var startYmdHm = moment(startYmd.format("YYYY-MM-DD") + "T" + moment().format("HH") + ":00:00"); var endYmdHm = 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(); } } /** * 年月日の形式を変換する. */ function formatNengappi(nengappi, flg) { var ret = nengappi.replace("年", "-").replace("月", "-").replace("日", ""); if (flg == 1){ ret = nengappi.replace("年", "-").replace("月", "-").replace("日", "T").replace("時",":").replace("分",":").replace(" ",""); } return ret; }
サーバー側(Python)
追加したカラムもクライアントから連携して登録するように修正。
また、クライアントにJSONを返すようにして、画面の情報も更新します。
Main.py
class RegistSchedule(RequestHandler): """ スケジュール登録 """ def initialize(self): logging.info("RegistSchedule [initialize]") def post(self): logging.info("RegistSchedule [post]") mysql = MySQLUtil() param = json.loads(self.request.body) allday = "TRUE" if param["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
カラム追加したので、その修正も行う。
def create_db(self): """ データベース、及び必要なテーブルを作成します. :return: """ with closing(mysql.connector.connect(**self.config)) as conn: 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() def insert_data(self, data): """ データを登録します :param ticker: :return: """ with closing(mysql.connector.connect(**self.config)) as conn: 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() def get_schedule(self, year_month, user_cd=""): """ データ(テーブル)をデータフレームに変換してかえします :return: """ result = [] with closing(mysql.connector.connect(**self.config)) as conn: 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 + "')" if user_cd != "": sql += " AND USER = '" + user_cd + "'" sql += " ORDER BY USER_CD, ID" c.execute(sql) for r in c.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'] }) return result
起動してみる
日付をクリック、またはドラッグすると予定入力用のダイアログフォームが表示されます。
予定を入力して登録ボタンをクリックします。
登録完了メッセージが表示されれば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
よろしくお願い致します。