【Pytnon】FullCalendarで登録済みの予定を更新、削除してみる
おはようございます。
祝日のない6月もそろそろ終わり、夏休みの時期が近づいてきましたね。
といっても会社員の皆さんは多くても1週間ちょっとくらいしか休みが取れないでしょうけど、
まあ楽しみですよね。
私もお盆の時期に休みを取る予定で
8月のカレンダーを眺めていたんですが、山の日が土曜日と被ってることに気づいて驚愕。
出来たばっかなのに酷いですよねぇ。
話が逸れましたが、
前回に引き続きカレンダーページを弄っていきます。
今回は、既に登録されているイベントを更新したり削除したりできるようにします。
プログラムは前回のものを流用しまっす。
https://www.doraxdora.com/blog/2018/06/24/post-5027/
スポンサーリンク
画面の修正
登録フォームに、隠しタグ、ボタンを追加。
Main.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 id="dialogTitle" class="modal-title">スケジュール登録</h4>
</div>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-md-2">
<label for="">タイトル</label>
<input id="userCd" type="hidden" value="" />
<input id="scheduleId" type="hidden" value="" />
</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-4">
<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 id="registButton" type="button" class="btn btn-primary">登録</button>
<button id="updateButton" type="button" class="btn btn-primary">更新</button>
<button id="deleteButton" type="button" class="btn btn-primary">削除</button>
<button type="button" class="btn btn-default" data-dismiss="modal">閉じる</button>
</div>
</div>
</div>
</div>
</body>
</html>
/**
* ページ初期処理.
*/
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) {
// 日付選択された際のイベント
// ダイアログタイトル設定
$("#dialogTitle").text("スケジュール登録");
// タイトル初期化
$("#inputTitle").val("");
// 備考初期化
$("#inputDescription").val("");
// ボタン制御
$("#registButton").show();
$("#updateButton").hide();
$("#deleteButton").hide();
// ダイアログ表示
$('#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);
$("#dialogTitle").text("スケジュール詳細");
// ユーザーCD設定
$("#userCd").val(event.user_cd);
// スケジュールID設定
$("#scheduleId").val(event.id);
// タイトル設定
$("#inputTitle").val(event.title);
// 備考設定
$("#inputDescription").val(event.description);
// ボタン制御
$("#registButton").hide();
$("#updateButton").show();
$("#deleteButton").show();
// ダイアログ表示
$('#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(event.start);
var endYmd = moment(event.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日"));
},
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(), 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');
}
/**
* 予定入力フォームの更新ボタンクリックイベント.
*/
function updateSchedule() {
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 = {
user_cd: $("#userCd").val(),
id: $("#scheduleId").val(),
title: $('#inputTitle').val(),
start: startYmd.format("YYYY-MM-DDTHH:mm:ss"),
end: endYmd.format("YYYY-MM-DDTHH:mm:ss"),
allDay: allDayCheck,
description: $('#inputDescription').val()
};
filter = {
user_cd: $("#userCd").val(),
id: $("#scheduleId").val()
}
$.ajax({
url: "http://localhost:8080/update",
type: "POST",
data: JSON.stringify(eventData),
success: function(jsonResponse) {
// 再描画
$('#calendar').fullCalendar('removeEvents');
$('#calendar').fullCalendar('renderEvents', $.parseJSON(jsonResponse) )
alert("予定を更新しました。");
},
error: function() {
}
});
}
$('#calendar').fullCalendar('unselect');
}
/**
* 予定入力フォームの削除ボタンクリックイベント.
*/
function deleteSchedule() {
// ユーザCD
var userCd = $("#userCd").val();
// スケジュールID
var id = $("#scheduleId").val();
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 = {
user_cd: $("#userCd").val(),
id: $("#scheduleId").val(),
};
$.ajax({
url: "http://localhost:8080/delete",
type: "POST",
data: JSON.stringify(eventData),
success: function(jsonResponse) {
// 再描画
$('#calendar').fullCalendar('removeEvents');
$('#calendar').fullCalendar('renderEvents', $.parseJSON(jsonResponse) )
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;
}
import json
import logging
import os
import tornado.ioloop
from tornado.web import RequestHandler
from tornado.options import options
from Utils.MySQLUtil import MySQLUtil
from datetime import datetime
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("Main.html")
class GetCalendar(RequestHandler):
"""
カレンダー取得
"""
def initialize(self):
logging.info("GetCalendar [initialize]")
def get(self):
logging.info("GetCalendar [get]")
mysql = MySQLUtil()
data = mysql.get_schedule("201806", "")
self.write(json.dumps(data, default=support_datetime_default))
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)
# ユーザCD、IDを設定して返す
param["userCd"] = "001"
param["id"] = id
self.write(param)
class UpdateSchedule(RequestHandler):
"""
スケジュール更新
"""
def initialize(self):
logging.info("UpdateSchedule [initialize]")
def post(self):
logging.info("UpdateSchedule [post]")
mysql = MySQLUtil()
param = json.loads(self.request.body)
allday = "TRUE" if param["allDay"] else "FALSE"
id = mysql.get_next_id('001')
data = [
param["title"],
param["start"],
param["end"],
"",
"",
"",
allday,
param["description"],
param["user_cd"],
param["id"]
]
mysql.update_data(data)
data = mysql.get_schedule("201806", "")
self.write(json.dumps(data, default=support_datetime_default))
class DeleteSchedule(RequestHandler):
"""
スケジュール削除
"""
def initialize(self):
logging.info("DeleteSchedule [initialize]")
def post(self):
logging.info("DeleteSchedule [post]")
mysql = MySQLUtil()
param = json.loads(self.request.body)
data = [
param["user_cd"],
param["id"]
]
mysql.delete_data(data)
data = mysql.get_schedule("201806", "")
self.write(json.dumps(data, default=support_datetime_default))
def support_datetime_default(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError(repr(o) + " is not JSON serializable")
app = tornado.web.Application([
(r"/", MainHandler),
(r"/getCalendar", GetCalendar),
(r"/regist", RegistSchedule),
(r"/update", UpdateSchedule),
(r"/delete", DeleteSchedule),
],
template_path=os.path.join(os.getcwd(), "templates"),
static_path=os.path.join(os.getcwd(), "static"),
)
if __name__ == "__main__":
options.parse_command_line()
app.listen(8080)
logging.info("server started")
tornado.ioloop.IOLoop.instance().start()
import mysql.connector
import logging
from contextlib import closing
class MySQLUtil:
"""
MySQL 操作用クラス
"""
def __init__(self, host="localhost", port="3306", user="USER01", password="USER01", database="DB01"):
self.config = {
"host": host,
"port": port,
"user": user,
"password": password,
"database": database
}
self.create_db()
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 data:
: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 update_data(self, data):
"""
データを更新します
:param data:
:return:
"""
with closing(mysql.connector.connect(**self.config)) as conn:
c = conn.cursor()
# データ登録
sql = "UPDATE TBL_SCHEDULE SET TITLE = %s, " \
" START = %s, " \
" END = %s, " \
" TEXTCOLOR = %s, " \
" COLOR = %s, " \
" URL = %s, " \
" ALLDAY = %s, " \
" DESCRIPTION = %s " \
"WHERE " \
" USER_CD = %s AND " \
" ID = %s"
c.execute(sql, data)
c.close()
conn.commit()
def delete_data(self, data):
"""
データを削除します
:return:
"""
logging.info("delete_data")
with closing(mysql.connector.connect(**self.config)) as conn:
c = conn.cursor()
# データクリア
sql = "DELETE FROM TBL_SCHEDULE WHERE USER_CD = %s AND ID = %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
def get_next_id(self, user_cd=""):
"""
データ(テーブル)をデータフレームに変換してかえします
:return:
"""
result = 0
with closing(mysql.connector.connect(**self.config)) as conn:
c = conn.cursor(dictionary=True)
sql = "SELECT MAX(ID) + 1 AS ID FROM TBL_SCHEDULE WHERE USER_CD = '" + user_cd + "'"
c.execute(sql)
result = c.fetchone()
return result[r"ID"]
起動してみる
登録済みのイベントをクリックします。
登録されている予定の内容がダイアログに表示され、更新、削除ボタンが表示されます。
とりあえず削除してみましょうか。
無事に予定が削除されました。
まとめ
これでとりあえず、予定の登録、修正、削除まで出来るようになりました。
次回は月表示以外の部分もみていきたいと思います。
ではでは。
ディスカッション
コメント一覧
まだ、コメントがありません