ブログをGoogle App Engineへ移行してみる13
2011/01/17 21:54:58
カテゴリーまわりの続きです。カテゴリーは別モデルで前回CRUDを作りましたので、エントリーに紐付ける処理を実装していきましょう。
「Entry」モデルクラスにプロパティを追加します。
/model.py
違いは
・「StringListProperty()」 → キー文字列を配列で入れる
・「ListProperty()」 → キーオブジェクトを配列で入れる
ぐらいの漠然な感じで捉えているのですが、例えば「ListProperty()」の場合はカテゴリーのキーをPOSTしてきた場合、「db.Key()」メソッドで単なるキー文字列をキーオブジェクトに変換してからでないとエラーが出る。
実際のところ、一長一短がありコード量の違いしか目に見える違いはない。恐らくパフォーマンスで差が出るものと今のところは推測しておく。
で、「StringListProperty()」を選んだ理由は、編集画面から選んだカテゴリー値をそのままEntryに挿入できるから楽だというだけである。
次にエントリーの新規投稿画面、編集画面にてカテゴリーを選択できるようにしてみよう。pythonファイルの方は単に全てのカテゴリーをひっぱってきてテンプレートに値を渡すだけなので簡単です。テンプレートのソースは以下のとおり。
/template_admin/entries/new.html

登録してあるカテゴリーをならべてチェックボックスで複数選択できるようなUIです。選んだカテゴリーのキー文字列が配列でEntryの「categories」に入るイメージですね。
さて、チェックしたこの値を受け取るのはどうしたらいいのでしょうか?「categories」という値が複数POSTされることになります。
複数の値を受け取るには「self.request.get_all()」メソッドを使います。つまり
self.request.get_all("categories")
ですね。ちなみに他と同じように「self.request.get()」とすると複数選んだとしても最初の値しかひろえません。
/admin.py
ちなみに「entry.categories」にはどのような値が入っているのでしょうか?もちろんSDK Consoleでデータを確認することはできますが、「self.request.get_all("categories")」ではどんな値が投げられているのか確認したい時がありますよね?
そういう時はログに情報を出して確認するとわかりやすいです。

赤文字で囲ったように、キー文字列が配列となって取得できています。ここでは「PHP」と「Python」を選びました。
新規登録は簡単にできましたが、問題は編集ですね。
編集画面で登録してあるカテゴリーにはチェックをつける必要があります。テンプレートからは簡単なテンプレ言語しか使えませんので、複雑なことはできません。jQueryでチェックをつけるという手もありあそうですが、ここではテンプレートに渡す値を予め編集しておくという手法をとることにします。
/admin.py
6〜12行目が肝なのですが、要はカテゴリーの要素に「select」という新しいプロパティを追加して、これにこのエントリーの登録カテゴリーであるかどうかを「True」で持たせています。
実現しているのは「map」関数でひっぱってきたカテゴリー配列に対して自作関数「mapit」を食わせて、既存の値を再構成しつつ新たな「select」を設定しているのです。
こうしておくと、
/template_admin/entries/edit.html

こんな感じで。
あとは編集の登録処理といえど、新規投稿と同じなので難しくないはずです。
最後に記事一覧のそれぞれにどのカテゴリーを登録しているかを表示してみます。「Entry」の「categories」プロパティにはカテゴリーのキー文字列が配列で入っていることになりますね。それらを分解してキーからカテゴリー名を取得してくればよさそうです。
/model.py
/template_admin/entries/index.html

ただし、コメントでもこのカテゴリーでもそうですが、テンプレートからメソッドを呼び出すのはパフォーマンスがよろしくないということなので、本当はテンプレートに渡す値の時点でHTMLで表示する情報全て持たせておくのが好ましいと思います。
が、今は今後の課題としておいておきましょう。
/model.py
categories = db.StringListProperty()最初「StringListProperty()」ではなく「ListProperty(item_type=db.Key)」で実装してみたのだが、正直どちらでも実現できることは同じでどちらがよいかはわからなかった。
違いは
・「StringListProperty()」 → キー文字列を配列で入れる
・「ListProperty()」 → キーオブジェクトを配列で入れる
ぐらいの漠然な感じで捉えているのですが、例えば「ListProperty()」の場合はカテゴリーのキーをPOSTしてきた場合、「db.Key()」メソッドで単なるキー文字列をキーオブジェクトに変換してからでないとエラーが出る。
実際のところ、一長一短がありコード量の違いしか目に見える違いはない。恐らくパフォーマンスで差が出るものと今のところは推測しておく。
で、「StringListProperty()」を選んだ理由は、編集画面から選んだカテゴリー値をそのままEntryに挿入できるから楽だというだけである。
次にエントリーの新規投稿画面、編集画面にてカテゴリーを選択できるようにしてみよう。pythonファイルの方は単に全てのカテゴリーをひっぱってきてテンプレートに値を渡すだけなので簡単です。テンプレートのソースは以下のとおり。
/template_admin/entries/new.html
{% extends "../layout.html" %}
{% block script_function %}{% endblock %}
{% block h1 %}新規投稿{% endblock %}
{% block contents %}
<form action="/admin/new" method="post">
■タイトル
<input type="text" name="title" value="" size="40" />
■内容
<textarea name="body" cols="40" rows="10"></textarea>
■カテゴリー
{% for category in categories %}
<input type="checkbox" name="categories" value="{{ category.key }}" />{{ category.name|escape }}
{% endfor %}
<input type="submit" value="投稿する" />
</form>
{% endblock %}14〜16行目を追加しました。ちなみにこの時点は「edit.html」も同じです。
登録してあるカテゴリーをならべてチェックボックスで複数選択できるようなUIです。選んだカテゴリーのキー文字列が配列でEntryの「categories」に入るイメージですね。
さて、チェックしたこの値を受け取るのはどうしたらいいのでしょうか?「categories」という値が複数POSTされることになります。
複数の値を受け取るには「self.request.get_all()」メソッドを使います。つまり
self.request.get_all("categories")
ですね。ちなみに他と同じように「self.request.get()」とすると複数選んだとしても最初の値しかひろえません。
/admin.py
class EntryNew(webapp.RequestHandler):
def get(self):
categories = Category.all().order('-created_at')
template_values = {
'categories': categories
}
path = os.path.join(os.path.dirname(__file__), 'template_admin/entries/new.html')
self.response.out.write(template.render(path, template_values))
def post(self):
entry = Entry()
entry.title = self.request.get("title")
entry.body = self.request.get("body")
entry.categories = self.request.get_all("categories")
entry.put()
self.redirect("/admin/")「EntryNew」ハンドラークラスを上記のように書き足します。ちなみに「entry.categories」にはどのような値が入っているのでしょうか?もちろんSDK Consoleでデータを確認することはできますが、「self.request.get_all("categories")」ではどんな値が投げられているのか確認したい時がありますよね?
そういう時はログに情報を出して確認するとわかりやすいです。
import loggingファイル上部にこれを書き足して、例えば「self.request.get_all("categories")」を確認したかったら
logging.info(self.request.get_all("categories"))と書くとログに以下のようにでます。
赤文字で囲ったように、キー文字列が配列となって取得できています。ここでは「PHP」と「Python」を選びました。
新規登録は簡単にできましたが、問題は編集ですね。
編集画面で登録してあるカテゴリーにはチェックをつける必要があります。テンプレートからは簡単なテンプレ言語しか使えませんので、複雑なことはできません。jQueryでチェックをつけるという手もありあそうですが、ここではテンプレートに渡す値を予め編集しておくという手法をとることにします。
/admin.py
class EntryEdit(webapp.RequestHandler):
def get(self, key):
entry = db.get(db.Key(key))
categories = Category.all().order('-created_at')
def mapit(category):
val = {
'key': category.key(),
'name': category.name,
'select': str(category.key()) in entry.categories
}
return val
template_values = {
'entry': entry,
'categories': map(mapit, categories)
}
path = os.path.join(os.path.dirname(__file__), 'template_admin/entries/edit.html')
self.response.out.write(template.render(path, template_values))「EntryEdit」ハンドラークラスを上記のようにします。6〜12行目が肝なのですが、要はカテゴリーの要素に「select」という新しいプロパティを追加して、これにこのエントリーの登録カテゴリーであるかどうかを「True」で持たせています。
実現しているのは「map」関数でひっぱってきたカテゴリー配列に対して自作関数「mapit」を食わせて、既存の値を再構成しつつ新たな「select」を設定しているのです。
こうしておくと、
/template_admin/entries/edit.html
{% extends "../layout.html" %}
{% block script_function %}{% endblock %}
{% block h1 %}編集{% endblock %}
{% block contents %}
<form action="/admin/edit/{{ entry.key }}" method="post">
■タイトル
<input type="text" name="title" value="{{ entry.title|escape }}" size="40" />
■内容
<textarea name="body" cols="40" rows="10">{{ entry.body|escape }}</textarea>
■カテゴリー
{% for category in categories %}
<input type="checkbox" name="categories" value="{{ category.key }}"{%if category.select %} checked="checked"{%endif%} />{{ category.name|escape }}
{% endfor %}
<input type="submit" value="変更する" />
</form>
{% endblock %}「category.select」がTrueの場合はこのカテゴリーが選択されているのでチェックがつくというわけですね。
こんな感じで。
あとは編集の登録処理といえど、新規投稿と同じなので難しくないはずです。
最後に記事一覧のそれぞれにどのカテゴリーを登録しているかを表示してみます。「Entry」の「categories」プロパティにはカテゴリーのキー文字列が配列で入っていることになりますね。それらを分解してキーからカテゴリー名を取得してくればよさそうです。
/model.py
class Entry(db.Model):
title = db.StringProperty()
body = db.TextProperty()
categories = db.StringListProperty()
updated_at = db.DateTimeProperty(auto_now=True)
created_at = db.DateTimeProperty(auto_now_add=True)
def jst_created_at(self):
return self.created_at + datetime.timedelta(hours=9)
def comments(self):
return Comment.all().ancestor(self).order('created_at')
def cats(self):
try:
return db.get(self.categories)
except:
return []「Entry」モデルクラスに「cats」というメソッドを追加します。本当は「categories」としたいところですが、データモデルのプロパティとかぶってしまうのでNGです。/template_admin/entries/index.html
{% extends "../layout.html" %}
{% block script_function %}
<script type="text/javascript">
function deletecheck(){
if(window.confirm('本当に削除してもよろしいですか?\nデータは完全に削除されます。')){
return true;
}else{
return false;
}
}
</script>
{% endblock %}
{% block h1 %}投稿一覧{% endblock %}
{% block contents %}
{% for entry in entries %}
<h2>{{ entry.title|escape }}</h2>
<p>{{ entry.jst_created_at|date:"Y年m月d日 H時i分" }}</p>
<p>カテゴリー:
{% for cat in entry.cats %}
{{ cat.name|escape }}
{% endfor %}
</p>
<p>
[<a href="/admin/edit/{{ entry.key }}">編集</a>]
[<a href="/admin/delete/{{ entry.key }}" onclick="return deletecheck()">削除</a>]
</p>
<hr />
{% endfor %}
{% endblock %}テンプレートから上記で設定したメソッドが使えるので後はforで回して表示するだけです。
ただし、コメントでもこのカテゴリーでもそうですが、テンプレートからメソッドを呼び出すのはパフォーマンスがよろしくないということなので、本当はテンプレートに渡す値の時点でHTMLで表示する情報全て持たせておくのが好ましいと思います。
が、今は今後の課題としておいておきましょう。