hama_duのブログ

ノンジャンル記事置き場

Google App Engine用フレームワークtipfyの紹介とチュートリアル その3

このエントリについて

Google App Engine用に作られたフレームワーク「tipfy」の使い勝手がよかったので
チュートリアル形式で簡単に紹介をしたいと思います。

前々回と前回の続きの内容になってます。
その1:http://d.hatena.ne.jp/hama_DU/20101206/1291644589
その2:http://d.hatena.ne.jp/hama_DU/20101212/1292120224

前回まで

  • エントリ一覧機能
  • エントリ作成機能

を作りました。

今回は

  • エントリ編集機能
  • エントリ削除機能
  • その他いろいろ

を作ります。

エントリ編集画面を作る

エントリ編集画面といっても、実はエントリ作成画面が使い回せます。
フォームから与えられたデータを受け取って、エントリを更新する機能を作ればいいことになります。


しかし、使い回せるとはいっても、編集機能を作るには
フォーム入力画面に一工夫してやる必要があります。
それは、エントリのデータストアの一意なIDをフォームに埋め込むことです。
Google App Engineでは、データストア内のオブジェクトを表す一意なIDとしてキーオブジェクトが用意されています。

リクエストハンドラを編集

EditHandlerを編集します。
リクエストパラメータ「entry_key」を受け取り、コンテキストに追加します。

  def get(self):
    entry_key = self.request.args.get('entry_key', '')
    context = {
      'form': FindForm(self.request),
      'entry_key': entry_key,
    }
    return render_response('entry/edit.html', **context)
テンプレートを編集
      <form method="post" action="{{ url_for('entry/edit') }}" enctype="multipart/form-data">
        <input type="hidden" name="entry_key" value="{{ entry_key }}">
        <p>
          {{ form_field(form.title) }}
        </p>
        <p>
          {{ form_field(form.content) }}
        </p>
        <p>
          <input type="submit" value="投稿!"/>
        </p>
      </form>

受け取ったパラメータ entry_key を隠し持っておきます。

リクエストハンドラを編集
  def post(self):
    sentform = FindForm(self.request)
    context = {
      'form': sentform,
    }
    if not sentform.validate():
      return render_response('entry/edit.html', **context)

    entry_key = self.request.form.get('entry_key', None)
    if not entry_key:
      Entry(
        title=sentform.title.data,
        content=sentform.content.data,
      ).put()
    else:
      entry_model = Entry.get(db.Key(entry_key))
      if not entry_model:
        return render_response('entry/edit.html', **context)

      entry_model.title = sentform.title.data
      entry_model.content = sentform.content.data
      entry_model.put()
      
    return render_response('entry/complete.html')

フォームがpostされてきたとき、entry_keyを見てもしも空っぽだったら新規登録、
値が入っていたらそのキーを持つオブジェクトを探してきて更新します。
もし、値が入っていても対応するオブジェクトが見つからない場合は更新しません。


エントリのオブジェクトを取得してる部分

      entry_model = Entry.get(db.Key(entry_key))

についてですが、ここで entry_key はあくまでもパラメータから取得した文字列なので、検索するときはキーの文字列に対応したオブジェクトを作らなければなりません。ここでは、

      db.Key(entry_key)

とすることで文字列に対応したオブジェクトを作成しています。詳しくは
http://code.google.com/intl/ja/appengine/docs/python/datastore/keyclass.html
をご覧ください。

エントリ一覧画面の編集

エントリ一覧画面に、編集画面へのリンクを追加します。
モデルのキーオブジェクトは、

model.key()

とすることでアクセス可能です。このキーをパラメータに渡します。

{% extends 'layout.html' %}

{% block body %}
  {% if not entry_list %}
    現在、まだエントリはありません
  {% else %}
    <table border="1">
      <tr>
        <th>title</th>
        <th>created</th>
        <th>edit</th>
      </tr>
      {% for entry in entry_list %}
        <tr>
          <td>{{ entry.title|e }}</td>
          <td>
          {% if entry.created_at %}
            {{ entry.created_at.replace(tzinfo=utc).astimezone(jst) }}
          {% endif %}
          </td>
          <td>
            <a href="{{ url_for('entry/edit') }}?entry_key={{ entry.key() }}">edit</a>
          </td>
        </tr>
      {% endfor %}
    </table>
  {% endif %}
{% endblock %}

エントリ削除機能を作る

設計

/entry/delete?entry_key=(削除したいエントリーのキー)
にアクセスすると、指定されたキーを持つエントリが削除されるようにします。

リクエストハンドラの編集

DeleteHandlerを追加します。

class DeleteHandler(RequestHandler):
  def get(self):
    entry_key = self.request.args.get('entry_key', '')
    entry_model = Entry.get(db.Key(entry_key))
    if not entry_model:
      return self.redirect('entry/list')

    entry_model.delete()
    context = {
    }
    return render_response('entry/deletecomplete.html')

編集機能のpostメソッドと同じようなコードになります。
もし、指定されたキーに対するオブジェクトが見つからなかったら一覧画面へリダイレクトさせます。

テンプレートを作る
{% extends 'layout.html' %}

{% block body %}
  <p>削除が完了しました。</p>
  <p><a href="{{ url_for('entry/list') }}">一覧へ</a></p>
{% endblock %}
ルーティング設定
    rules = [
        Rule('/entry/list', endpoint='entry/list', handler='apps.entry.handlers.ListHandler'),
        Rule('/entry/edit', endpoint='entry/edit', handler='apps.entry.handlers.EditHandler'),
        Rule('/entry/delete', endpoint='entry/delete', handler='apps.entry.handlers.DeleteHandler'),
    ]
エントリ一覧画面の編集

エントリ一覧画面に、削除画面へのリンクを追加します。

{% extends 'layout.html' %}

{% block body %}
  {% if not entry_list %}
    現在、まだエントリはありません
  {% else %}
    <table border="1">
      <tr>
        <th>title</th>
        <th>created</th>
        <th>edit</th>
        <th>delete</th>
      </tr>
      {% for entry in entry_list %}
        <tr>
          <td>{{ entry.title|e }}</td>
          <td>
          {% if entry.created_at %}
            {{ entry.created_at.replace(tzinfo=utc).astimezone(jst) }}
          {% endif %}
          </td>
          <td>
            <a href="{{ url_for('entry/edit') }}?entry_key={{ entry.key() }}">edit</a>
          </td>
          <td>
            <a href="{{ url_for('entry/delete') }}?entry_key={{ entry.key() }}">delete</a>
          </td>
        </tr>
      {% endfor %}
    </table>
  {% endif %}
{% endblock %}

雑多なこと

hello_worldアプリケーションの削除

もう使わないので、削除しちゃっても大丈夫です。

時刻を整形して表示する

一覧画面で、エントリの作成時刻が表示されていますが、

2010-12-10 12:58:41.952249+09:00

という風に、秒数やタイムゾーンなどの余計な情報が含まれているので、整形して表示させるようにしてみます。

一覧画面のテンプレートを修正します。

      {% for entry in entry_list %}
        <tr>
          <td>{{ entry.title|e }}</td>
          <td>
          {% if entry.created_at %}
            {{ entry.created_at.replace(tzinfo=utc).astimezone(jst).strftime('%Y/%m/%d %H:%M') }}
          {% endif %}
          </td>
          <td>
            <a href="{{ url_for('entry/edit') }}?entry_key={{ entry.key() }}">edit</a>
          </td>
          <td>
            <a href="{{ url_for('entry/delete') }}?entry_key={{ entry.key() }}">delete</a>
          </td>
        </tr>
      {% endfor %}

strftimeを使うことで形式を指定して時間を文字列に変換できます。
詳しくは
http://www.python.jp/doc/2.5/lib/module-time.html
をご覧ください。

エントリ一覧を更新日時の降順で表示させる

一般的に、ブログは最新のものから表示されるようになってます。
エントリ数が多くなった場合スクロールするのが面倒なので、
このシステムでも最新のエントリが一番上に表示されるようにしてみます。


リクエストハンドラのデータフェッチ部分を変更します。

class ListHandler(RequestHandler):
  def get(self):
    entry_list = Entry.all().order('-updated_at').fetch(100)
    context = {
      'entry_list': entry_list,
      'utc': utc(),
      'jst': jst(),
    }
    return render_response('entry/list.html', **context)

all() でデータストアのクエリオブジェクトが得られるので、
クエリオブジェクトに用意された関数 order() を使うだけで並び順が指定できます。
カラムのデータの昇順に並び替えたい時は

order(カラム名)

と指定するのですが、カラム名の先頭に「-」をつけると降順で並び替えてくれます。

今回はここまで。

次回以降は未定です。
その4から先をやるとすれば、

  • ユーザ別にブログを管理する機能(+CSRF対策)
  • Twitter連携

あたりのネタになりますかね。


良いお年を。