[TurboGears] ブログを作成する。その6

注意

この記事は、id:SumiTomohiko:20070120:1169310317の続きです。

カレンダー

今回は、tgdiaryにカレンダーをつけることにします。カレンダーは、以下の外観をしています。

仕様は、以下のように定めました。

  1. 記事を表示したら、その記事がある年月をカレンダーに表示します。
  2. 「年」「月」ドロップダウンリストで、表示する年月を選択します。このとき、変化するのはカレンダーだけで、記事はそのままにします。
  3. 記事がある日付は、リンクにします。そのリンクをクリックしたら、その日付の記事を表示します。
  4. 日付のうち、土曜日は青色で、日曜日は赤色で表示します。
  5. 「今月」ボタンをクリックしたら、操作した時点の年月を表示します。

このカレンダーのポイントは2番目の点で、カレンダーの表示と記事の表示はそれぞれ独立させたいと考えています。これを実現するには、URLに記事の日時とカレンダーの年月の両方を与えるという方法があります。しかし、これだとカレンダーを切替えるたびに画面全体を書き直さなくてはならず、使い勝手がよくありません。この問題を解決するため、今回はAjaxを用いることにします。言うまでもなく、Ajax(とDHTML)ならば、画面を部分的に書き換えることができます。

ブラウザとサーバ間のやりとり

ここで、ブラウザとサーバの間で、どのようにして情報をやりとりするかを定めます。ブラウザがサーバに与えるパラメータは、ユーザIDと年、月です。サーバからは、ある日付において、記事があるかないかが得られればよいです。そこで、以下のように定めました。

  1. ブラウザは、/calendar?user=&year=&month=にアクセスします。, はそれぞれ、ユーザIDと、カレンダーで表示する年、月です。
  2. サーバは、JSON形式のハッシュで応答します。
  3. 応答には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) &amp;&amp; (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 &lt; 31; i++) {
                var day = i + 1;
                var name = 'day' + day;
                if (i &lt; 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 = '&lt;span class=&quot;sunday&quot;&gt;' + s + '&lt;span&gt;';
                    } else if (day_of_week == 6) {
                        s = '&lt;span class=&quot;saturday&quot;&gt;' + s + '&lt;span&gt;';
                    }
                    if (exists[i]) {
                        var query = queryString(['user', 'year', 'month', 'day', 'span'], [${value['user']}, year, month, day, 1]);
                        s = '&lt;a href=&quot;/date?' + query + '&quot;&gt;' + s + '&lt;/a&gt;';
                    }
                    $(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 &lt; 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 &lt; days_of_month">
                        <span py:if="exists">
                            <a href="/date?user=${value['user']}&amp;year=${value['current_year']}&amp;month=${value['current_month']}&amp;day=${day + 1}&amp;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 &lt;= calendar.weekday(value['current_year'], value['current_month'], day + 1) &lt;= 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 &lt;= calendar.weekday(value['current_year'], value['current_month'], day + 1) &lt;= 4">
                                ${day + 1}
                            </span>
                        </span>
                    </span>
                </span>
                <span py:if="days_of_month &lt;= day">&nbsp;</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)

以上で、ブログにカレンダーがつきました。