渡米生活。(日記)

渡米生活。本家から切り離しました。あまり渡米生活に関係のないプログラムネタや音楽ネタなど。

Wordpressで特定のカテゴリ記事のみのイベントカレンダー(イベント名つき)を作成する

Wordpressでイベントカレンダーを表示するプラグインはたくさんあるのですが、多くがカスタム投稿を利用するタイプでして、フリーテーマを使っていたりすると具合が悪いケースがあります。

Wordpressでイベントの告知をするのに、私は通常の投稿記事を使っています。こうすると、綺麗なデザインのフリーテーマとほぼ衝突することなく使えて、イベント情報をスライド表示したりできるからです。
この部分を崩さないまま、イベントカレンダーを作る方法はないか、と探して、結局そこそこ簡単に実装できることがわかりました。
しかも、コードの部分はPHP Code For Posts などのプラグインを入れてそこに書きますので、テーマに手を加える必要がなく、着せ替えも思いのままです。

見た目は以下のスクリーンショットのような形になります。イベント名をクリックするとその記事に飛びます。
上の年月の横にある矢印(>)をクリックすると、翌月のカレンダーを表示します。
(<をクリックすると前月のカレンダーを表示しますが、この実装では本日以前のイベントは見えないようになっていますので、ただのカレンダーが表示されるだけです)

実装はこちらのページで見てください。


ただし、これをやるには、自分である程度のルールを作って記事を投稿する必要があります。

1)カレンダーに表示したい記事のカテゴリを設定し、イベント用の記事の名前の付け方にルールを設ける。

Wordpressの中でイベント告知を投稿記事を使って書く方法は、大きく分けて2つあります。
1つは投稿記事を未来のイベントの日付で作り、No Future Postなどのプラグインで未来の投稿も表示するやり方。これも悪くないのですが、投稿記事の投稿日がイベント日になってしまうため、スライダーでの表示順をいじるのが難しくなる、という問題があります。
多くのスライダーは新しい順に表示してしまうので、1ヶ月先のイベントの記事をスライダーのトップに表示したいのに、1年先のイベントが1番目になってしまう、といったような問題です。

そこで、私は現在、記事のタイトルそのものに日付を入れてしまう、という方法をとっています。これだと、投稿の日付は自由に設定できるので、そこをいじればスライダーの表示順を変えることができます。
ヘボいかな、と思いましたが、やってみると、スライダーでイベント日がタイトルの先頭に見えるのは、お客さんにもわかりやすくて、それほど悪くありません。

しかし、そのためには、記事のタイトルの付け方をあらかじめ決めておく必要があります。
この自前ルールは、あとでコードを書くときに必要になりますので、一度決めたら変えないこと。私はこんな感じにしました。

  1. イベント記事のタイトルは、必ず yyyy/mm/dd(またはyyyy/m/d)で始める
  2. そのあとに半角スペースをあけて、記事のタイトルを書く
  3. 記事タイトル中には半角スペースを含んではいけない。全角スペースはOK
  4. 数日にわたるイベントは、yyyy/mm/dd-dd とする。月をまたぐ場合は、yyyy/mm/dd-mm/dd、年をまたぐ場合はyyyy/mm/dd-yyyy/mm/ddとする。

あ、書いてて気づいた。今のプログラムだと月をまたいで数日にわたるイベントが対応できませんね。あとで直します。。変更しました。

いずれにしても、この方法の良いところは、自前で決めたルールでいくらでもあとで調整が可能なところです。別にタイトルをいじる方法ではなくても、カスタムフィールドの値を使うでもOK。カスタムフィールドに必要な情報を全部分類して入れておけば、あとでプログラムを書くときにかなり楽ができます。

ちなみに、カレンダーに表示するイベント記事用には、EVENTカテゴリ(別にどんな名前でも良い)を割り当てておけば、このカテゴリをスライダーにセットしてトップページにスライド表示できます。私はさらに、カレンダーには表示したいけど、トップスライダーには出したくないイベント用にSCHEDULEカテゴリも作りました。

2)終わったイベントを自動で非公開にするプラグイン(Enable Post Expiration)を入れる。

終わったイベントがいつまでも表示されているのはよくありませんので、私はPost Expiratorというプラグインを使っています。
これは、指定の日時がきたらイベントの公開状態を非公開や下書きなどに自動で変更してくれるプラグインです。「Enable Post Expiration」の項目にチェックを入れなければ適用されませんので、イベント情報記事以外が影響を受けることはありません。

このプラグインを入れたら、カレンダーに表示したい記事全ての「Enable Post Expiration」にチェックを入れ、取り下げる日時を設定します。私は単純に、イベント日当日の23時59分にセットしていますが、イベントが終わってもしばらくはカレンダーに表示しておきたい、という方はもう少し遅らせてもよいかと思います。

Enebale Post Expirationについては、以下のリンクで解説されています。

※ちなみに、このプラグインの使用は必須ではありません。要はWP_Queryでカレンダーに表示する記事を選べれば良いので、単純にあるカテゴリの記事だけ表示して、取り下げるのは自分でやる、という方はそれでも大丈夫です。また、自分でカスタムフィールドなどに別の日付を設定して、それを使うでもOK。

3)PHP Code For Posts など、管理画面からPHPのショートコードを作成できるプラグインを入れる。

カレンダーを作る関数はテーマの中のfunction.phpに書くのが定石ですが、それではテーマの変更が必要になります。テーマがアップデートしたら変更も消えてしまってよくありません。
別にファイルを作り、プラグインとして登録する手もありますが、このカレンダーはかなり使用方法に特化しているので、そこまで一般化して他のサイトで使えるようにする手間が面倒です。

というわけで、管理画面からPHPプログラムを作成しショートコードを作ってくれるプラグインを入れます。別にショートコードを作ってくれるプラグインならなんでもOKです。

PHP Code For Posts については、以下のリンクで解説されています。

4)ショートコードを作成

カレンダーの作成ですが、カレンダーの表示部分についてはほとんど以下のサイトで公開されていたものの丸写しです(使わせていただいてありがとうございます!)

動画で学ぶプログラミング・アプリ開発 Coding with Sara

変更したのは以下の2点のみ。

  1. カレンダーを月曜始まりに変更
  2. カレンダーに紐付ける記事を選ぶ部分

このカレンダーの例題は日曜始まりですが、イベントカレンダーは月曜始まりの方が都合がいいので、以下の1行を加えました。

//曜日を月曜始まりに変更 
$youbi = ($youbi == 0) ? 6 : $youbi - 1;

これは、その前のdate関数で月の1日が日曜なら0、月曜なら1…、土曜なら6の数字が返ってくるので、月曜なら0、火曜なら1、…日曜なら6、に変更するためのものです。こうすると、(この例題プログラムでは)月曜から日曜までのカレンダーを作ってくれます。

5)カレンダーに表示するイベントを選ぶ。

月や年をまたぐイベントに対応し、かつ同日に複数イベントが入った場合にも全部表示するようにしたら、結構面倒になりました…。
具体的には、さきにカレンダーに表示するイベントを選んで一度バッファにFILLしておき、あとでカレンダーに書く、という作業をしています。

このため、指定の範囲内の日付($startdayから$enddayまで)に同じイベントをフィルする関数をつくりました。
$databufは3次元の配列です。

function mycalendar_fillevents($databuf, $startday, $endday, $title, $url) {
	if ($startday > $endday) {
		print 'error, startday > endday <br>';
	}
	$curday = $startday;
	for ($i = 0; $i < $endday - $startday + 1; $i++) {
		$curday = $startday + $i; // カレンダーの日付
		$countid = count($databuf[$curday]); // 同日に複数イベントが入った場合のID
		$databuf[$curday][$countid]["title"] = $title;
		$databuf[$curday][$countid]["url"] = $url;
	}
	return $databuf;
}

さらに、以下の部分でイベントの抽出をしています。
何をやっているかはコメントを見てください。

//
// イベントを抽出するクエリを作成。
// 下の例題は、記事の種類は「投稿」、カテゴリーはeventsかschedule、
// 抽出した結果をソートするのに、Post Expiratorの取り下げ日時を
// 使っている。Post Expiratorが使うカスタムフィールドのキー値が
// _expiration-dateです。これを、meta_value_num、つまり数値として
// 解釈し、ASC(昇順)でソートしろ、ということ。
//
// これは私がPost Expiratorの取り下げ日時をイベント当日
// にしているためで、そうでない場合は別にカスタムフィールドに値を
// 入力してコントロールした方が良いと思います。
// 一番単純な方法は、yyyymmddの書式で_event_dateとか適当なキー値で
// カスタムフィールドに値を入力しておけば、ソートも楽になるかと。
//
//
// extract event list
//
$args = array(
	'post_type'  => 'post',
	'category_name' => 'events,schedule',
	'meta_key'   => '_expiration-date',
	'orderby'    => 'meta_value_num',
	'order'      => 'ASC',
	'posts_per_page' => -1	
	);

// The Query
$the_query = new WP_Query( $args );

// イベント日とそのイベント名、対応する記事へのリンクをおさめる配列を用意。
$databuf = array();

// 選んだ投稿をループしながら、必要な情報を保存。
if ( $the_query->have_posts() ) {
	
	while ( $the_query->have_posts() ) {
		$the_query->the_post();
		$url = get_the_permalink();

		global $post, $id;
		// 非公開記事には頭に「非公開」とついてしまうので、それを削除。テストは非公開記事で行える。
		$articletitle = trim(str_replace('非公開:','', get_the_title()));
		// 半角スペースでタイトルを分割。前半が日付情報、後半がイベントタイトルになる。
		$splits = explode(' ', $articletitle);
		$title = $splits[1];
		
		$datearray = explode('-', $splits[0]);
		
		$startymd = explode('/', $datearray[0]);
		$startyear = $startymd[0];
		$startmonth = $startymd[1];
		$startday = $startymd[2];
		$theday = new DateTime();
		$theday->setDate((int)$startyear, (int)$startmonth, (int)$startday);
		$startym = $theday->format('Ym');
		
		// $startymが現在の表示年月より大きい場合は、残りのイベントも全て同条件で現在のカレンダーの表示範囲外。
		// なので、すぐにloopを出る。
		if ($startym > $page_ym) {
			break;
		}
			
		if (count($datearray) == 1 && $startym == $page_ym) {
			// yyyy/mm/ddのフォーマット。
			// 1日のみのイベントなので、現在のカレンダーの年月に合致した場合のみデータベースにFILLする
			$databuf = mycalendar_fillevents($databuf, $startday, $startday, $title, $url);
			
		} else if (count($datearray) > 1) {
			// 複数日イベント
			// yyyy/mm/dd-dd、yyyy/mm/dd-mm/dd、 yyyy/mm/dd-yyyy/mm/dd のいずれかのフォーマット
			// $datearray[0] = yyyy/mm/dd
			// $datearray[1] は dd, mm/dd, yyyy/mm/dd のいずれか。
			
			$endymd = explode('/', $datearray[1]);
			$endymdcounts = count($endymd);
			
			// try simple case first.
			if ($endymdcounts == 1) {
				// イベント開始日と終了日が同じ月。現在の表示カレンダーの年月のものだけFILLする
				// フォーマットは yyyy/mm/dd-dd.
				if ($page_ym != $startym) {
					// month doesn't match through event period. go to next post.
					continue;
				}
				$endday = $endymd[0];
				$databuf = mycalendar_fillevents($databuf, $startday, $endday, $title, $url);
				
			} else {
				// イベント終了日が開始日と同じ年月にない場合は、イベント期間中のうち表示中のカレンダー年月のみFILLする
				// フォーマットは yyyy/mm/dd-mm/dd または yyyy/mm/dd-yyyy/mm/dd.
				$endyear = $startyear;
				$endmonth = $startmonth;
				$endday = $startday;
				if ($endymdcounts == 2) {
					$endmonth = $endymd[0];
					$endday = $endymd[1];
				} else if ($endymdcounts == 3) {
					$endyear = $endymd[0];
					$endmonth = $endymd[1];
					$endday = $endymd[2];	
				}
								
				$theday->setDate((int)$endyear, (int)$endmonth, (int)$endday);
				$endym =  $theday->format('Ym');
				
				if ($page_ym > $endym) {
					// イベント終了日が現在の表示年月より前の場合は、このイベントをスキップ
					continue;
				} 
				
				$curym = $startym;
				$curyear = $startyear;
				$curmonth = $startmonth;
				// 開始日付は1日かイベント開始日のどちらかになる
				$curday = ($page_ym == $startym) ? $startday : 1;
				
				// 開始年月から終了年月までループ
				while ($curym <= $page_ym) {
					
					if ($page_ym == $curym) {
						// カレンダーの表示中年月と一致したのでFILLする。
						// update start day and end day.
						$startday = $curday;
						$last_day = $day_count;
						// 開始日付は月の最後の日かイベント終了日のどちらか
						$endday = ($curym == $endym) ? $endday : $last_day;
						$databuf = mycalendar_fillevents($databuf, $startday, $endday, $title, $url);
					} 
					
					//次月を取得
					if ($curmonth == 12) {
						//次の月は翌年1月
						$curyear += 1;
						$curmonth = 1;
					} else {
						$curmonth += 1;
					}
					$theday->setDate((int)$curyear, (int)$curmonth, (int)$curday);
					$curym = $theday->format('Ym');
				}
			}
		}
	}
}

//最後にクエリのリセットを忘れずに
wp_reset_postdata();


これら組み合わせたプログラム全体は以下のようになります。

<?php 

date_default_timezone_set('Asia/Tokyo');

//前月と次月を表示する際は、GETで値を受け取る
if (isset($_GET['ym'])) {
	$page_ym = $_GET['ym'];

}else{
	$page_ym = date('Ym');
}


//形式チェック
$timestamp = strtotime($page_ym . "01"); // yyyymmddの書式にする


if ($timestamp === false) {
	$timestamp = time();
}


//今日の日付
$today = date('Ymd',time());
$thismonth = date('n', $timestamp);
$thisyear = date('Y', $timestamp);


//HTML表示用の日付
$html_title = date('Y年 n月',$timestamp);

//前月と次月を取得  mktime(hour, minute, second, month, day, year)
$prev = date('Ym', mktime(0, 0, 0, date('m', $timestamp)-1, 1 , date("Y", $timestamp)));
$next = date('Ym', mktime(0, 0, 0, date('m', $timestamp)+1, 1, date("Y", $timestamp)));

//対象月は何日あるか
$day_count = date('t', $timestamp);

//1日は何曜日か 0:日 1:月 .... 6:土
$youbi = date('w', mktime(0, 0, 0,date('m', $timestamp), 1, date("Y", $timestamp)));

//曜日を月曜始まりに変更 
$youbi = ($youbi == 0) ? 6 : $youbi - 1;

function mycalendar_fillevents($databuf, $startday, $endday, $title, $url) {
	if ($startday > $endday) {
		print 'error, startday > endday <br>';
	}
	$curday = $startday;
	for ($i = 0; $i < $endday - $startday + 1; $i++) {
		$curday = $startday + $i; // カレンダーの日付
		$countid = count($databuf[$curday]); // 同日に複数イベントが入った場合のID
		$databuf[$curday][$countid]["title"] = $title;
		$databuf[$curday][$countid]["url"] = $url;
	}
	return $databuf;
}

//
// extract event list
//
$args = array(
	'post_type'  => 'post',
	'category_name' => 'events,schedule',
	'meta_key'   => '_expiration-date',
	'orderby'    => 'meta_value_num',
	'order'      => 'ASC',
	'posts_per_page' => -1	
	);

// The Query
$the_query = new WP_Query( $args );

// this buffer will be 3dim array
$databuf = array();

// The Loop
if ( $the_query->have_posts() ) {
	
	while ( $the_query->have_posts() ) {
		$the_query->the_post();
		$url = get_the_permalink();

		global $post, $id;
		$articletitle = trim(str_replace('非公開:','', get_the_title()));
		$splits = explode(' ', $articletitle);
		$title = $splits[1];
		
		$datearray = explode('-', $splits[0]);
		
		$startymd = explode('/', $datearray[0]);
		$startyear = $startymd[0];
		$startmonth = $startymd[1];
		$startday = $startymd[2];
		$theday = new DateTime();
		$theday->setDate((int)$startyear, (int)$startmonth, (int)$startday);
		$startym = $theday->format('Ym');
		
		// $startymが現在の表示年月より大きい場合は、残りのイベントも全て同条件で
		// 現在のカレンダーの表示範囲外なのでloopを出る
		if ($startym > $page_ym) {
			break;
		}
			
		if (count($datearray) == 1 && $startym == $page_ym) {
			// 1日のみのイベントなので、現在のカレンダーの年月に合致した場合のみデータベースにFILLする
			// single day event. yyyy/mm/dd
			// year and month matched. fill the event to the buffer.
			$databuf = mycalendar_fillevents($databuf, $startday, $startday, $title, $url);
			
		} else if (count($datearray) > 1) {
			// 複数日イベント
			// multiple days event.
			// date format is yyyy/mm/dd-dd or yyyy/mm/dd-mm/dd or yyyy/mm/dd-yyyy/mm/dd
			// $dataarray[0] = yyyy/mm/dd
			// $dataarray[1] could be dd, mm/dd, yyyy/mm/dd
			
			$endymd = explode('/', $datearray[1]);
			$endymdcounts = count($endymd);
			
			// try simple case first.
			if ($endymdcounts == 1) {
				// イベント開始日と終了日が同じ月。現在の表示カレンダーの年月のものだけFILLする
				// the event will be held within a month.
				// date format is yyyy/mm/dd-dd.
				if ($page_ym != $startym) {
					// month doesn't match through event period. go to next post.
					continue;
				}
				$endday = $endymd[0];
				$databuf = mycalendar_fillevents($databuf, $startday, $endday, $title, $url);
				
			} else {
				// イベント終了日が開始日と同じ年月にない場合は、イベント期間中のうち表示中のカレンダーの年月のみFILLする
				// the event will be held through at least two different months.
				// date format is yyyy/mm/dd-mm/dd or yyyy/mm/dd-yyyy/mm/dd.
				$endyear = $startyear;
				$endmonth = $startmonth;
				$endday = $startday;
				if ($endymdcounts == 2) {
					$endmonth = $endymd[0];
					$endday = $endymd[1];
				} else if ($endymdcounts == 3) {
					$endyear = $endymd[0];
					$endmonth = $endymd[1];
					$endday = $endymd[2];	
				}
								
				$theday->setDate((int)$endyear, (int)$endmonth, (int)$endday);
				$endym =  $theday->format('Ym');
				
				if ($page_ym > $endym) {
					// イベント終了日が現在の表示年月より前の場合はこのイベントをスキップ
					// current calendar is out of bound of this event. go to next post.
					continue;
				} 
				
				$curym = $startym;
				$curyear = $startyear;
				$curmonth = $startmonth;
				// 開始日付は1日かイベント開始日のどちらか
				$curday = ($page_ym == $startym) ? $startday : 1;
				
				// 開始年月から終了年月までループ
				while ($curym <= $page_ym) {
					
					if ($page_ym == $curym) {
						// OK, fill calendar for this month.
						// update start day and end day.
						$startday = $curday;
						$last_day = $day_count;
						// 開始日付は月の最後の日かイベント終了日のどちらか
						$endday = ($curym == $endym) ? $endday : $last_day;
						$databuf = mycalendar_fillevents($databuf, $startday, $endday, $title, $url);
					} 
					
					//次月を取得
					if ($curmonth == 12) {
						//次の月は翌年1月
						$curyear += 1;
						$curmonth = 1;
					} else {
						$curmonth += 1;
					}
					$theday->setDate((int)$curyear, (int)$curmonth, (int)$curday);
					$curym = $theday->format('Ym');
				}
			}
		}
	}
}

/* Restore original Post Data */
wp_reset_postdata();

//カレンダー作成準備
$weeks = array();
$week = '';

//空白を追加
//例) 1日が水曜日だった場合、カレンダーの月曜日から火曜日に空白を入れる
$week .= str_repeat('<td></td>', $youbi);


for ($day=1; $day <= $day_count; $day++, $youbi++){
	if (array_key_exists($day, $databuf)) {
		$week .= '<td>' . $day;
		for ($i = 0; $i < count($databuf[$day]); $i++) { 
			$week .= '<br><a href="' . $databuf[$day][$i]['url'] . '"><small>'.$databuf[$day][$i]['title'].'</small></a>';
		}	
	} else {
		$week .= '<td>'. $day ;
	}
	$week .= '</td>';
	
	//曜日が日曜日、または全ての日付のtdの作成が終わったら
	if($youbi % 7 == 6 OR $day == $day_count){

		//全ての日付のtdの作成が終わったら、残りに空白を追加
		if($day == $day_count){
			$week .= str_repeat('<td></td>', 6 - ($youbi % 7));
		}

		//1週間分のtdをまとめた$weekを$weeks配列に入れる。
		$weeks[] = '<tr>'.$week.'</tr>';
		//新しい週を作成するために$weekを空にする。
		$week = '';
	}

}
?>
	<h4><a href="?ym=<?php echo $prev;?>">&lt;</a>&nbsp;&nbsp;<?php echo $html_title;?>&nbsp;&nbsp;<a href="?ym=<?php echo $next;?>">&gt;</a></h4>
	<br>
	<table id="wp-calendar">
		<thead>
		<tr>
			<th width="14%"></th>
			<th width="14%"></th>
			<th width="14%"></th>
			<th width="14%"></th>
			<th width="14%"></th>
			<th width="14%"></th>
			<th width="14%"></th>
		</tr>
		</thead>
		<tbody>
	 	<?php
		 	//カレンダー表示
		 	foreach ($weeks as $week){
				echo $week;
			}
		?>
		</tbody>
	</table>

カレンダー用のテーブルに、id="wp-calendar"を設定してますが、もちろん別のIDでかまいません。
なぜこうしたかというと、以下のページで紹介されていたカレンダーがカッコよくて、そのままcssを拝借したかったからです。
テーマによっては、自前CSSの追加を許してくれるものがあるので、、、

WordPressでイベントカレンダー wpxtreme


このコードをPHP Code For Postsにセットし、ショートコードを作成します。
あとは、投稿なりページなりでショートコードを入力すれば、その場所にカレンダーが表示されます。

テキストウィジェットでも表示できますが、このカレンダーはイベント名を表示するので、サイドバーなどでは縦に伸びてしまって見づらくなります。
ウィジェットで小さく表示するには、別にショートコードを作成した方がよさそうです。

というわけで、この記事はほとんど人様のコードを拝借して書いたものです。
参考にさせていただいた皆様に感謝!