[TurboGears] ブログを作成する。その6
注意
この記事は、id:SumiTomohiko:20070120:1169310317の続きです。
カレンダー
今回は、tgdiaryにカレンダーをつけることにします。カレンダーは、以下の外観をしています。
仕様は、以下のように定めました。
- 記事を表示したら、その記事がある年月をカレンダーに表示します。
- 「年」「月」ドロップダウンリストで、表示する年月を選択します。このとき、変化するのはカレンダーだけで、記事はそのままにします。
- 記事がある日付は、リンクにします。そのリンクをクリックしたら、その日付の記事を表示します。
- 日付のうち、土曜日は青色で、日曜日は赤色で表示します。
- 「今月」ボタンをクリックしたら、操作した時点の年月を表示します。
このカレンダーのポイントは2番目の点で、カレンダーの表示と記事の表示はそれぞれ独立させたいと考えています。これを実現するには、URLに記事の日時とカレンダーの年月の両方を与えるという方法があります。しかし、これだとカレンダーを切替えるたびに画面全体を書き直さなくてはならず、使い勝手がよくありません。この問題を解決するため、今回はAjaxを用いることにします。言うまでもなく、Ajax(とDHTML)ならば、画面を部分的に書き換えることができます。
ブラウザとサーバ間のやりとり
ここで、ブラウザとサーバの間で、どのようにして情報をやりとりするかを定めます。ブラウザがサーバに与えるパラメータは、ユーザIDと年、月です。サーバからは、ある日付において、記事があるかないかが得られればよいです。そこで、以下のように定めました。
- ブラウザは、/calendar?user=
&year= &month= にアクセスします。 と , はそれぞれ、ユーザIDと、カレンダーで表示する年、月です。 - サーバは、JSON形式のハッシュで応答します。
- 応答にはexists要素があり、これの値は配列です。この配列の要素は、「インデックス + 1」日に記事があるかないかを表します。記事がある場合はTrue, ない場合はFalseとします。
calendarメソッド
tgdiary/controllers.pyのRootクラスに、以下のメソッドを追加します。
@expose(format="json") @validate(validators={'user': validators.Int(), 'year': validators.Int(), 'month': validators.Int()}) def calendar(self, user, year, month): # TODO: Validate user, year and month. return dict(exists=self._get_article_exists_array(user, year, month))
@expose(format="json")で、出力の形式をJSONに指定しています。実際に記事の有無を返すのは、以下の_get_article_exists_arrayメソッドです。
def _get_article_exists_array(self, user, year, month): import calendar tmp, days_of_month = calendar.monthrange(year, month) exists = [False] * days_of_month from_time = datetime.datetime(year, month, 1, 0, 0, 0) to_time = from_time + datetime.timedelta(days_of_month) articles = list(Article.select(AND(Article.q.userID == user, Article.q.created >= from_time, Article.q.created < to_time))) for article in articles: exists[article.created.day - 1] = True return exists
Calendarウィジェット
カレンダーは、ウィジェットとして作成しました。必要なJavaScriptも、ウィジェットで出力します。
class CalendarWidget(widgets.Widget): params = ['year_field', 'month_field'] def __init__(self, *args, **kw): super(CalendarWidget, self).__init__(*args, **kw) self.year_field = widgets.SingleSelectField('year', attrs={'onchange': 'year_onchange(this)', 'onblur': 'year_onchange(this)'}, validator=validators.Int()) self.month_field = widgets.SingleSelectField('month', attrs={'onchange': 'month_onchange(this)', 'onblur': 'month_onchange(this)'}, options=[month for month in range(1, 13)]) template = ''' <?python import calendar ?> <?python import datetime ?> <div class="calendar" xmlns:py="http://purl.org/kid/ns#"> <form> <script language="JavaScript"> function get_days_of_month(year, month) { var days = [31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if (month != 2) { return days[month - 1]; } if ((year % 4 == 0) && (year % 100 != 0)) { return 29; } else { return 28; } } function update_calendar(year, month) { var deferred = loadJSONDoc('/calendar', { user: ${value['user']}, year: year, month: month }); var success = function(data) { var exists = data.exists; var days_of_month = get_days_of_month(year, month); for (var i = 0; i < 31; i++) { var day = i + 1; var name = 'day' + day; if (i < days_of_month) { var s = day; var date = new Date(year, month - 1, day); var day_of_week = date.getDay(); if (day_of_week == 0) { s = '<span class="sunday">' + s + '<span>'; } else if (day_of_week == 6) { s = '<span class="saturday">' + s + '<span>'; } if (exists[i]) { var query = queryString(['user', 'year', 'month', 'day', 'span'], [${value['user']}, year, month, day, 1]); s = '<a href="/date?' + query + '">' + s + '</a>'; } $(name).innerHTML = s; } else { $(name).innerHTML = ''; } } }; var failed = function() { alert('カレンダーを更新できません。'); }; deferred.addCallbacks(success, failed); } function get_selected_value(select) { return select.options[select.selectedIndex].value; } function year_onchange(select) { var year = get_selected_value(select); var month = get_selected_value(select.form.month); update_calendar(year, month); } function month_onchange(select) { var year = get_selected_value(select.form.year); var month = get_selected_value(select); update_calendar(year, month); } function select_value(select, value) { for (var i = 0; i < select.options.length; i++) { if (select.options[i].value == value) { select.selectedIndex = i; return; } } } function this_month_onclick(button) { var year = ${value['current_year']}; var month = ${value['current_month']}; select_value(button.form.year, year); select_value(button.form.month, month); update_calendar(year, month); } </script> <?python days_of_month = calendar.monthrange(value['current_year'], value['current_month']) ?> <table border="0"> <tr> <td> <input name="this_month" type="button" value="今月" onclick="this_month_onclick(this)"/> </td> <td> ${year_field.display(value['current_year'], options=[year for year in range(value['oldest_year'], datetime.datetime.now().year + 1)])}年 </td> <td>${month_field.display(value['current_month'])}月</td> <td width="20" py:for="day, exists in enumerate(value['articles'])"> <span id="day${day + 1}"> <span py:if="day < days_of_month"> <span py:if="exists"> <a href="/date?user=${value['user']}&year=${value['current_year']}&month=${value['current_month']}&day=${day + 1}&span=1"> <span class="saturday" py:if="calendar.weekday(value['current_year'], value['current_month'], day + 1) == 5"> ${day + 1} </span> <span class="sunday" py:if="calendar.weekday(value['current_year'], value['current_month'], day + 1) == 6"> ${day + 1} </span> <span py:if="0 <= calendar.weekday(value['current_year'], value['current_month'], day + 1) <= 4"> ${day + 1} </span> </a> </span> <span py:if="not exists"> <span class="saturday" py:if="calendar.weekday(value['current_year'], value['current_month'], day + 1) == 5"> ${day + 1} </span> <span class="sunday" py:if="calendar.weekday(value['current_year'], value['current_month'], day + 1) == 6"> ${day + 1} </span> <span py:if="0 <= calendar.weekday(value['current_year'], value['current_month'], day + 1) <= 4"> ${day + 1} </span> </span> </span> </span> <span py:if="days_of_month <= day"> </span> </td> </tr> </table> </form> </div> '''
CalendarWidgetクラスのparams変数には、CalendarWidgetクラスのメンバ変数のうち、テンプレートに渡すパラメータの名前を指定します。これにより、年と月のドロップダウンリストがテンプレートに渡されます。
このウィジェットを使うには、以下のようにします。
<script type="text/javascript" src="/tg_js/MochiKit.js"></script> (略) ${calendar_widget.display(calendar_value)}
calendar_valueは、カレンダーの表示に必要な情報をまとめた辞書です。以下のメソッドで生成し、dateメソッドの戻り値に加えています。
def _get_calendar_value(self, user): now = datetime.datetime.now() current_year = now.year current_month = now.month articles = list(Article.select(AND(Article.q.userID==user), orderBy=Article.q.id)[:1]) if 0 < len(articles): oldest_year = articles[0].created.year else: oldest_year = current_year articles = self._get_article_exists_array(user, current_year, current_month) return dict(user=user, current_year=current_year, current_month=current_month, oldest_year=oldest_year, articles=articles)
以上で、ブログにカレンダーがつきました。
参考文献
- 1.0/IntroductionToWidgets - TurboGears Documentation
- id:aodag:20070114:1168807255