金星翻車魚(キンボシマンボウ)のデザインを変更してみました
あまりにも殺風景だったので、ちょっとデザインをいじってみました。といっても、ちゃんとWebデザインできるスキルは持ち合わせていないので、ちょっとましになったか程度ですけれど。
汎用ビューによる更新・削除ページ
前回で、TODOタスクの追加と一覧までは実現できたので、今回からは編集と削除処理を作っていきます。
削除処理については、汎用ビューのdjango.views.generic.create_update.delete_objectを使います。利用手順としては、まず、views.pyに削除処理用のメソッドを追加します。
/todo/views.py
from django.views.generic.create_update import delete_object def delete_task(request): if not request.user.is_authenticated(): return render_to_response(request, "login.html") # ログインユーザのタスクかどうかチェック task = Task.get(request.POST["key"]) if (task.user != request.user): return HttpResponseRedirect(reverse('todo.views.list_tasks')) return delete_object(request, Task, object_id=request.POST["key"], post_delete_redirect=reverse('todo.views.list_tasks'))
メソッド中盤でログインユーザのタスクかどうかをチェックしていますが、これがないとkeyの値さえわかれば別ユーザのTODOタスクを削除することが可能になります。
次に、urls.pyで、/todo/deleteにアクセスがあったらこのメソッドが呼ばれるようにします。
/todo/urls.py
urlpatterns = patterns('todo.views', (r'^$', 'list_tasks'), (r'^create$', 'add_task'), (r'^delete$', 'delete_task'),
最後に、テンプレート内に削除ボタンを設置します。
/todo/templates/task_list.html
<table> <tr><th>タイトル</th><th>締切</th><th>削除</th></tr> {% for task in object_list %} <tr><td>{{ task.title }}</td><td>{{ task.limit_time|date:"Y-m-d" }}</td> <form action="delete" method="post"><td><input type="hidden" name="key" value="{{ task.key }}"><input type="submit" value="削除"></td></form> {% endfor %} </table>
これで削除機能が出来上がりました。
同じ要領で、編集機能を追加します。編集処理には、汎用ビューのdjango.views.generic.create_update.update_objectを利用します。まず、views.pyに編集用のメソッドを追加します。
/todo/views.py
from django.views.generic.create_update import update_object def update_task(request): if not request.user.is_authenticated(): return render_to_response(request, "login.html") # ログインユーザのタスクかどうかチェック task = None if (request.method == "GET"): task = Task.get(request.GET["key"]) else: task = Task.get(request.POST["key"]) if (task.user != request.user): return HttpResponseRedirect(reverse('todo.views.list_tasks')) return update_object(request, form_class=TaskForm, object_id=task.key(), post_save_redirect=reverse('todo.views.list_tasks'))
ログインユーザのタスクかどうかのチェックにちょっと手間をかけていますが、基本的には削除処理と同じ流れです。
次に、urls.pyを編集し、/todo/updateにアクセスしたらこのメソッドが呼ばれるようにします。
/todo/urls.py
urlpatterns = patterns('todo.views', (r'^$', 'list_tasks'), (r'^create$', 'add_task'), (r'^delete$', 'delete_task'), (r'^update$', 'update_task'), )
最後に、テンプレートを編集し、TODOタスクのタイトルをクリックしたら編集画面に行くようにします。
/todo/templates/task_list.html
<table> <tr><th>タイトル</th><th>締切</th><th>削除</th></tr> {% for task in object_list %} <tr><td><a href="update?key={{ task.key }}">{{ task.title }}</a></td><td>{{ task.limit_time|date:"Y-m-d" }}</td> <form action="delete" method="post"><td><input type="hidden" name="key" value="{{ task.key }}"><input type="submit" value="削除"></td></form> {% endfor %} </table>
と、ここで、manage.py runserverで開発サーバを立ち上げて確認すると、編集時に"key"がないと怒られてしまいました。そうか、Formに足さないといけませんね。というわけで、フォームにkeyを追加しました。
/todo/forms.py
class TaskForm(forms.ModelForm): user = forms.CharField(widget=forms.HiddenInput, required=False) title = forms.CharField(max_length=100, required=True, label=u"タイトル", help_text=u"(必須)") limit_time = forms.DateTimeField(required=False, label=u"締切", help_text=u"(任意)yyyy-mm-ddの形式で入力してください") key = forms.CharField(widget=forms.HiddenInput, required=False) class Meta: model = Task exclude = ('create_time')
これで動きましたので、manage.py updateでサーバへアップしました。
おそらくこれで最低限の機能はできたと思いますので、今後は、このTODO管理機能を拡張していくことを考えます。
Django Applicationを作る
ちょっと意味のあるアプリケーションとして、TODO管理機能を作っていくことにします。
Djangoと同じく、以下のコマンドでアプリケーションが生成されます。
manage.py startapp todo
これによりtodoフォルダができますので、中身を確認すると、以下のファイル・フォルダ群ができています。
media/ models.py templates/ views.py __init__.py
まず、Todo情報を表すモデルクラスを作成します。先ほど生成されたmodels.pyファイルに、以下のコードを加えました。
/todo/models.py
from google.appengine.ext import db from django.contrib.auth.models import User class Task(db.Model): user = db.ReferenceProperty(User) title = db.StringProperty() create_time = db.DateTimeProperty(auto_now_add=True) limit_time = db.DateTimeProperty()
次に、このTodo情報を閲覧するメソッドを用意します。先ほど生成されたviews.pyファイルに、以下のコードを加えました。
/todo/views.py
from django.views.generic.list_detail import object_list from ragendja.template import render_to_response from todo.models import Task def list_tasks(request): if not request.user.is_authenticated(): return render_to_response(request, "login.html") queryset = Task.all().filter("user", request.user) return object_list(request, queryset, paginate_by=20)
ここでは、DjangoのGeneric View(汎用ビュー)を利用することにしました。これに対応する一覧表示画面のテンプレートを用意します。汎用ビューの命名規則に沿った名前(この場合はtask_list.html)にしておけば、views.pyでテンプレート名を指定する必要はありません。
/todo/templates/task_list.html
{% extends "base.html" %} {% block head_title %}TODOリスト{% endblock %} {% block body_main %} <table> <tr><th>タイトル</th><th>締切</th></tr> {% for task in object_list %} <tr><td>{{ task.title }}</td><td>{{ task.limit_time|date:"Y-m-d" }}</td></tr> {% endfor %} </table> <div> {% if has_previous %} <a href="{% url todo.views.list_tasks %}?page={{ previous }}">前へ</a> {% endif %} {% if has_next %} <a href="{% url todo.views.list_tasks %}?page={{ next }}">次へ</a> {% endif %} </div> {% endblock %}
ページ下部の{% if has_previous %}...で始まる個所は、ページング処理を記述しています。
最後に、todoフォルダ内にurls.pyを用意し、トップのurls.pyから呼び出されるようにします。また、setting.pyでtodoアプリケーションに関して追記します。
/todo/urls.py
# -*- coding: utf-8 -*- from django.conf.urls.defaults import * from ragendja.urlsauto import urlpatterns urlpatterns = patterns('todo.views', (r'^$', 'list_tasks'), )
/urls.py
urlpatterns = patterns('', (r'^$', 'topapp.views.welcome'), (r'^todo/', include('todo.urls')),
/settings.py
INSTALLED_APPS = (
...中略...
'todo',
)
これで、Todo一覧のページができました。
同じ要領で、Todoタスクの追加ページを作ることにします。まず、Todoタスク追加フォームを表すModelFormクラスを作成します。todoフォルダ内に、新たにforms.pyファイルを作って、以下の内容を記述しました。
/todo/forms.py
from django import forms from todo.models import Task class TaskForm(forms.ModelForm): user = forms.CharField(widget=forms.HiddenInput, required=False) title = forms.CharField(max_length=100, required=True, label=u"タイトル", help_text=u"(必須)") limit_time = forms.DateTimeField(required=False, label=u"締切", help_text=u"(任意)yyyy-mm-ddの形式で入力してください") class Meta: model = Task exclude = ('create_time')
create_timeは自動生成されるものを利用するので、excludeに指定してユーザからの入力対象外としました。また、userにはログインユーザを登録する予定なので非表示(hidden)の入力フォームとしました。
次に、Todoタスク追加フォームを処理するメソッドをviews.pyに記述します。
/todo/views.py
from django.core.urlresolvers import reverse from django.views.generic.create_update import create_object def add_task(request): if not request.user.is_authenticated(): return render_to_response(request, "login.html") request.POST = request.POST.copy() request.POST["user"] = str(request.user.key()) return create_object(request, form_class=TaskForm, post_save_redirect=reverse('todo.views.list_tasks'))
ここでも、汎用ビューを利用しています。続いて、汎用ビューの命名規則に沿った追加フォームを作ります。
/todo/templates/task_form.html
{% extends "base.html" %} {% block head_title %}TODO追加{% endblock %} {% block body_main %} <form action="" method="post"> <table> {{ form.as_table }} <tr><td colspan="2"> <input type="submit" value="{% if object %}編集{% else %}追加{% endif %}" /> </td></tr> </table> </form> <a href="{% url todo.views.list_tasks %}">一覧へ戻る</a> {% endblock %}
最後に、urls.pyに以下の記述を追加して完了です。
/todo/urls.py
urlpatterns = patterns('todo.views', (r'^$', 'list_tasks'), (r'^create$', 'add_task'),
manage.py runserverコマンドで開発サーバを立ち上げて確認し、manage.py updateでアップロードしました。
このままだとTODOタスクは増えていく一方なので、次から、TODOタスクの編集と削除処理を作っていこうと思います。
ログイン認証に対応する
app-engine-patchを導入すると、ユーザアカウントの管理方法として、以下の3つが選択できるようになります。
今のところ、Googleアカウントの認証管理をそのまま利用するつもりでいますので、1番目を採用することにします。
app-engine-patchでGoogleアカウントの認証を利用する方法は簡単で、setting.pyの以下の箇所のコメントアウトを外します。また、その周辺には、独自アカウント管理(Django authentication)や併用アカウント管理(Hybrid Django/Google authentication)に関する行がありますので、これらの行がコメントアウトされていない場合はコメントアウトしておきます。
MIDDLEWARE_CLASSES = ( # Google authentication 'ragendja.auth.middleware.GoogleAuthenticationMiddleware', (中略) # Google authentication AUTH_USER_MODULE = 'ragendja.auth.google_models' AUTH_ADMIN_MODULE = 'ragendja.auth.google_admin' (中略) GLOBALTAGS = ( 'ragendja.templatetags.googletags',
これだけで、Googleアカウント認証を利用する準備が整います。あとは、ログイン/ログアウトページへのリンクを表示すればいいだけなので、base.htmlに以下のような行を追加します。
<div class="login"> {% if user.is_authenticated %} ログイン中:{{ user.username }} <a href="{% google_logout_url request.get_full_path %}">ログアウト</a> {% else %} <a href="{% google_login_url request.get_full_path %}">ログイン</a> {% endif %} </div>
例によって、manage.py runserverで開発サーバを動かしてローカル環境で確認したうえで、manage.py updateを実行して本番環境にアップロードしました。
これでほぼ、お膳立てはほぼ整いましたので、次からは簡単なアプリケーションを作っていきたいと思います。
アクセスカウンタの復活とtemplateの継承
app-engine-patchにおいてもModelはGoogleのモデルを使うようなので、前のアクセスカウンタ用のモデルをそのまま流用します。
前回からの変更点として、views.pyを新たに生成し、カウンタアップのロジックをここに移動させました。
views.py
# -*- coding: utf-8 -*- from ragendja.template import render_to_response from topapp.models import AccessCounter from google.appengine.ext import db def welcome(request): counter = get_counter() return render_to_response(request, 'welcome.html', {"counter":counter}) def get_counter(): def countup(): counter = AccessCounter.get_by_key_name("access_counter") if counter is None: counter = AccessCounter(key_name="access_counter", counter=0) counter.counter += 1 counter.put() return counter.counter return db.run_in_transaction(countup)
注目点としては、ragendja.template.render_to_responseというメソッドを使っているということでしょうか。これは、その名から推測できるように、django.shortcuts.render_to_responseメソッドと同様の機能を提供するapp-engine-patch内の便利メソッドです。役割はほぼ同じですが、プロジェクトごとのtemplateファイルを呼び出しやすくしたりといった利点があります。ただし、このメソッドのシグニチャとdjangoの同名ショートカットメソッドのシグニチャとが少し異なります。何が違うかといえば、細かいところは置いといて、ragendja.template.render_to_responseの方は最初の引数がrequestで第二引数がtemplateあること。一方、django.shortcuts.render_to_responseの方は第一引数がtemplateです。これに気づかず、しばらくハマってしまいました。まぁ、そんな失敗をする人の方が珍しいかもしれませんが。
ついでに、せっかくテンプレートを使っているので、テンプレートの継承を使ってみることにします。テンプレートの継承とは、Djangoのtemplateの強力な機能のひとつで、別のテンプレートを継承しその一部を置き換えるというようなことができます。今回は、以下のようなbase.htmlファイルを用意し、これをすべての大元のファイルとしました。
templates/base.html
<html> <head> <link rel="shortcut icon" href="favicon.ico"> <title>金星翻車魚 - {% block head_title %}{% endblock %}</title> {% block css %}<link type="text/css" rel="stylesheet" href="/css/main.css" />{% endblock %} {% block head %}{% endblock %} </head> <body> {% block body_header %} <h1>金星翻車魚(キンボシマンボウ)</h1> <hr> {% endblock %} {% block body_sub_header %}{% endblock %} {% block body_main %}{% endblock %} {% block body_sub_footer %}{% endblock %} {% block body_footer %} <hr /> <a href="/"><img src="/images/kinboshi_sunfish.jpg" alt="kinboshi-sunfish" width="120" height="30" style="border: none"></a> <img src="http://code.google.com/appengine/images/appengine-silver-120x30.gif" alt="Powered by Google App Engine" /> {% endblock %} </body> </html>
それぞれの{% block ... %}から{% endblock %}までの部分を、継承先のテンプレートで置き換えることができます。
templates/welcome.html
{% extends "base.html" %} {% block body_main %} <p>ようこそ</p> <p>あなたは{{counter}}番目のお客様です。</p> {% endblock %}
最初に{% extends "base.html" %}と記述することでbase.htmlを継承します。次に、継承元テンプレートから置き換えるブロック部分を記述します。上記のwelcome.htmlでは、base.htmlの中で{% block body_main %}で定義されている個所を置き換えています。
ついでに、File Not Found用のページも用意しておきます。
templates/404.html
{% extends "base.html" %} {% block head_title %}Page Not Found.{% endblock %} {% block body_main %} <p>お探しのページは存在しません。</p> <p><a href="/">TOPへ</a></p> {% endblock %}
最後に、urls.pyを書き換えて、/にアクセスされたらwelcomeページが出るようにし、それ以外の未定義のアドレスにアクセスが来たら404ページが出るようにします。
urls.py
urlpatterns = patterns('', (r'^.*$', 'views.welcome'), (r'.*$', 'django.views.generic.simple.direct_to_template', {'template': '404.html'}), )
とりあえず今日はここまで。次は、ログインあたりを作ってみようと思います。
app-engine-patchの導入
GettingStartedと、Unleash Django with app-engine-patchの記事を見て、app-engine-patchの使い方を理解しようとするも、分ったような分からないような。やっぱり実際に試してみないとよくわかりませんね。
というわけで、やってみます。
まず、Downloads app-engine-patchから、最新のsampleをダウンロードしてきます。これを書いている時点では、app-engine-patch-sample-1.0.zipが最新の安定版っぽかったので、これを落としてきました。
次に、このサンプルを展開してapp.yamlを開き、applicationの書き換えと、画像フォルダの設定の書き加えを行います。また、これまで使っていたフォルダからimagesフォルダをコピーしてきました。
app.yaml
application: kinboshi-sunfish (中略) - url: /images static_dir: images - url: /favicon.ico static_files: images/favicon.ico upload: images/favicon\.ico mime_type: image/x-icon
んで、templatesフォルダにこれまで使っていたwelcome.htmlをコピーし、urls.pyを開いて編集し、サイトにアクセスするとwelcome.htmlが呼び出されるようにしました。
urls.py
urlpatterns = auth_patterns + patterns('', ('^admin/(.*)', admin.site.root), (r'^.*$', 'django.views.generic.simple.direct_to_template', {'template': 'welcome.html'}), # Override the default registration form # url(r'^account/register/$', 'registration.views.register', # kwargs={'form_class': UserRegistrationForm}, # name='registration_register'), ) + urlpatterns
最後に、通常のDjangoと同じく、以下のコマンドを実行して開発サーバを起動します。
manage.py runserver
これで、http://localhost:8000にアクセスするとトップページが出ました。ただ、カウンタの処理を入れていないので、カウンタの数字は出ませんが。
ちなみに、http://localhost:8000/adminにアクセスすると、DjangoのAdminページが出ます。もちろん、この開発サーバはGoogle App Engine SDKのものを使っているので、http://localhost:8000/_ah/adminにアクセスすれば、ちゃんとGoogle App Engineの開発用サーバの管理サイトが見れます。
ところで、commonフォルダの他にもsampleにはいろいろなファイルが含まれているのですが、これってどこまで必要なんでしょうか?
ともかく、とりあえず動きそうなので、次は、カウンタを復活させるところから手をつけようと思います。なお、app-engine-patch導入後は、以下のコマンドで、Google App Engineにアップロードすることができます。今のところ使わなさそうなadminの部分をurls.pyからコメントアウトして、アップロードしました。
manage.py update
金星翻車魚(キンボシマンボウ)から、カウンタが消えてしまいました…。
Google App Engine上でDjangoを動かす
そろそろなんらかの動きのあるアプリケーションを作りだそうと思ったのですが、このままgoogle.appengine.ext.webappパッケージを使ってゴリゴリと書いていっていいのかとちょっと悩みました。躊躇した点は、以下。
- webappパッケージを使い続けると、Google App Engine専用のアプリケーションになってしまうのでは?
- ちゃんとしたWebアプリケーションを(最終的に)作るつもりであれば、Webアプリフレームワークを入れた方がいいのでは?
といっても、一つ目は、今のところ他の環境で動かすことは念頭にないですし、そもそもGoogle App Engineで動くアプリケーションを作ろうと始めたのに、それに依存した作りになるのを恐れるというのも矛盾している気もします。また、二つ目は、私がwebappパッケージやそれ以外のパッケージの機能をろくに知らないので、単なる印象論です。
まぁでも、Google App EngineとかPythonとかを仕事で使う可能性はほぼ無い状況ゆえ、これは完全に趣味の範囲です。なので、あまりはっきりとしたメリットは見い出せないわけではありますが、なんとなく楽しそうという理由でDjangoを動かすことにしました。
ざっと検索した範囲では、Google App Engine上でDjangoを動かすには、自力でちょっと工夫するか、Google App Engine Helper for Djangoを使うかが主だった方法のようです。ただ、いずれにせよGoogle App EngineでのデータストアがRDBMSではないため、それに依存した部分は動かないとのこと。せっかくのDjangoなのに、Adminが使えないのも悲しいなと思っていたところ、app-engine-patchなるものを知りました。
なんでも、Djangoの主要な機能をほとんどコード修正なくGoogle App Engine上でそのまま動かせることを目指したプロジェクトらしく、主要な機能の中にはAdminも入っています。今年の2/24に1.0がリリースされたということで、これはちょっと試してみたくなりました。
というわけで、次からはapp-engine-patchを調べて、試していこうと思います。なんかでも、なかなかアプリケーションを作りだすところまでいかないなぁ。