データストアに保存してある文字列をDjangoテンプレートとして使用する方法

(この記事の内容はGoogle App Engine SDK 1.6.6、Mac OX X 10.7.4で試しました)

いま、Google App Engine+Pythonで作成したアプリで、Djangoのテンプレートエンジンを使用しているのですが、テンプレートファイルの代わりに、データストアから取得してきた文字列をテンプレートとして使いたいのです。
Movable Typeのテンプレートの仕組みのようなものを実現したい)

いつもは次のように、template.render()の結果をresponse.out.write()しています。

self.response.out.write(template.render(path, template_values))

しかし、template.render()はテンプレートファイルへのパスしか渡すことができません。

そこでちょっと調べまして、

template_string = Template.get_by_id(id).content
template_object = template.Template(template_string)
template_values  = template.Context({
        'var1' : u'variable1',
        'var2' : u'variable2',
    })
self.response.out.write(template_object.render(template_values)

としてあげれば出来そうな感触を得ました。
しかし、これをやると

AssertionError: settings has not been configured in this thread

というエラーが発生してしまいました。
Tracebackを見ると、

    template_object = template.Template(template_string)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/_internal/django/template/__init__.py", line 156, in __init__
    if settings.TEMPLATE_DEBUG and origin is None:
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/_internal/django/conf/__init__.py", line 31, in __getattr__
    assert self.holder, 'settings has not been configured in this thread'

とあり、どうやら「Djangoの環境設定settingsにTEMPLATE_DEBUGの設定がないよ」ということのようでしたので、

from google.appengine._internal.django.conf import settings
settings.configure(
        TEMPLATE_DEBUG = True,
    )

としてあげたところ期待したとおりに画面が描画されました。

とは言え、テンプレートにデータストアから取得してきた文字列を使った場合、ファイルパスというものがありませんから、{% extends "path" %}や{% include "path" %}といったDjangoテンプレートエンジンの機能が使えません(pathが見つからないと無視され、何も出力しないようです)

継承やインクルードが使えないのではとても不便ですから、次はこのあたりを解決しなければならないのですが、これに関しては現時点でどう解決すればよいのか、皆目見当がついていません。

どなたか良い方法を教えていただけると助かります。
(そもそも、Djangoテンプレートエンジンをこういう風に使うのが間違い?)

OS X LionのFileVault 2は『ハードディスク全体』を暗号化しない

Mac OS X Lionからバージョンが上がったFileVault(FileVault 2)は、「ディスク全体で XTS-AES 128 暗号化を行なってユーザのデータの安全性を保ちます」とあるのですが、ここで言う「ディスク全体」はハードディスク装置の所謂『ディスク』を指すのではなく、OS X Lionがインストールされている『ボリューム全体』を指しているようです。

実際、Boot CampWindows 7をインストールし、FileVaultで暗号化した後にMacBook Proからハードディスク装置を取り外し、別のMacにUSB接続してみましたが、Macintosh HDボリュームをマウントしようとするとロック解除を求められるものの、BOOTCAMPボリュームは普通に中身を閲覧・操作できました。

FileVaultをオンにしても、OSが起動する前にパスワードを求められたりはしなかったので、まあそうなんじゃないかなーと薄々思ってはいたんですが、これはちょっと残念でした。

以下、試した手順と結果です。


(1)まっさらのHDDにMac OS X Lionをインストールする。

(2)Boot CampWindows 7をインストールする。

この時点で、HDDには"Macintosh HD"と"BOOTCAMP"というボリュームが存在する。

(3)Mac OS X LionのFileVaultでHDD全体を暗号化する。

(4)HDDを取り外して、別のMacOS X Lion)にUSB接続する。

(5)"Macintosh HD"のロック解除用パスワードを入力してください。というダイアログウィンドウが表示される。

ここで正しいパスワード(OS X Lionのインストール時に設定した管理者権限のユーザーのログインパスワード)を入力するとMacintosh HDとしてマウントされる。

間違ったパスワードを入力するとダイアログウィンドウがぷるぷる震えて再入力を促される。
n回間違ったら○○になる、みたいなことはない。

キャンセルボタンをクリックするとマウントされない。

(6)"BOOTCAMP"は特にロック解除などを求められることはなく、USB接続が認識されたら即マウントされる。

(7)取り外したHDDを別のPC(Windows 7)にUSB接続する。

(8)"BOOTCAMP"が(試したPCでは)Eドライブとして認識され、いつもの自動再生のオプション選択ウィンドウが表示される。

(9)自動再生オプションから「フォルダーを開いてファイルを表示」を選択するとExplorerで中身をいじることができる。


ということで、Boot CampWindows 7をインストールしている場合は、BOOTCAMPボリュームをTrueCryptオープンソース)やPGP Whole Disk EncryptionSymantec/有料)等を使って個別に暗号化するか、(この2つのソフトで可能かどうかは試していませんが)ディスク全体を暗号化するしかないようです。

MacBook Pro、MacBook AirにWindows 7だけをインストールする手順

プログラミングと関係ないですが、大いにハマったのでまとめておきます。
(費用がかかる工程があるので、ひと通り読んでから試してください)


DVDやUSBメモリのかたちでインストールディスクが付属していれば、それでブートしてディスクユーティリティを起動し、内蔵HDDからOS Xのボリュームを消去してWindowsをインストールすればよいだけです。


ただ、MacBook Pro/Airの最新モデルにはインストールディスクが付属していません。代わりにリカバリー用ユーティリティが用意されています。Command+Rキーを押下しながら電源を入れるとリカバリー用ユーティリティにアクセスでき、OS Xを修復インストールできるようになっているからです。


このリカバリー用ユーティリティは非常にスマートで、内蔵HDDを交換するなどしてOS Xのボリューム(Macintosh HD等)が存在しない場合には、インターネット経由でOS Xをダウンロードしてインストールしてくれます。


しかしながら、このリカバリー用ユーティリティでブートした場合、ディスクユーティリティを使用することはできますが、内蔵HDDのボリュームを操作することはできません。つまり、OS Xのボリュームを消去することができない(Windows"だけ"がインストールされた状態にすることができない)のです。

手順


1. OS X Lionのインストールディスクを作成する
2. Windowsデバイスドライバーを外部メディアに保存する
3. 内蔵HDDのOS Xボリュームを削除する
4. Windows 7をインストールする

1. OS X Lionのインストールディスクを作成する


意地でもWindowsだけをインストールしたい場合は、OS Xのインストールディスクをなんとかして作成しなければなりません。作成手順は次のURLに詳しく書いてあります。


(参考)Mac OS X Lionのリカバリディスクを作成する vayu


要約すると、App StoreOS X Lionを購入(2,600円)するとダウンロードできる『OS X Lion インストール.app』というファイルから、インストールディスクのディスクイメージを取り出し、USBメモリやDVD-Rに書き込みます。


このようにしてインストールディスクを作成すれば、DVDドライブからブートし、ディスクユーティリティを使って内蔵HDDのOS X用ボリュームを消去することでできます。


ただし、内蔵HDDのOS X用ボリュームを消去する前にWindows用のデバイスドライバーを用意しておかなければなりません。デバイスドライバーがないと、MacBook Pro/AirWindowsをインストールしてもまともに動作させることができません。

2. Windowsデバイスドライバーを外部メディアに保存する


Windowsデバイスドライバーは、OS Xにバンドルされている『Boot Camp アシスタント』というアプリケーションを使ってAppleからダウンロードし、USBメモリやDVD-Rに書き込みます。


手順としてはFinderでアプリケーション→ユーティリティの中からBoot Camp アシスタントを起動します。『最新のWindowsサポートソフトウェアをAppleからダウンロード』にチェックを入れて『続ける』、書き込むメディアの種類を選択して『続ける』、です。


ちなみにさっきから『デバイスドライバー』と書いていますが、要はBoot CampでWindowをインストールしたときに使うBoot Camp Toolsのことです。

3. 内蔵HDDのOS Xボリュームを消去する


さて、いよいよOS Xボリュームを消去します。Optionキーを押下しながら電源を入れ、起動ディスクを選択する画面になったらDVDドライブに作成したインストールディスクを挿入します。読み込みを少し待つと、画面にDVDのアイコンでMac OS Xが追加されますので、それをダブルクリックし、そのまま数分間待ちます。


しばらく待つと、OS Xのインストールユーティリティ画面が表示されます。最初に使用する言語を選択し、次に表示されるメニューから『ディスクユーティリティ』を選択し、起動します。


ディスクユーティリティが起動したら左サイドバーから内蔵HDDを選択し、右ペインのタブから『パーティション』を選択し、次のとおりに設定します。

  • ボリュームの方式:1パーティションを選択
  • 名前:空欄(空き領域)
  • フォーマット:空き領域
  • サイズ:そのまま(最大)
  • オプション...:マスター・ブート・レコードを選択


(参考)MacをWindows専用に


上のとおり設定したら『適用』をクリックします。
プログレスバーが表示されますが、すぐに終わります。終わったらディスクユーティリティを終了してOptionキーを押下しながら再起動します。

4. Windows 7をインストールする


Optionキーを押下しながら再起動したので、また起動ディスクを選択する画面が表示されます。DVDドライブからインストールディスクを取り出し、Window 7のインストールディスクを挿入します。すると『Windows 7』が選択できるようになりますので、ダブルクリックします。あとは普通にWindows 7のインストールウィザードに従ってインストールするだけです。


なお、MacBook ''Air''はDVDドライブを内蔵していませんので、DVDドライブをUSB接続する必要があります。ずっと昔に購入したバッファローのDVSM-X1216U2というDVD-RWドライブを接続したところ、そのドライブではWindow 7のインストールディスクからブートできませんでした。Apple純正のMacBook Air Super Driveを使用したら問題なくブートできました。何か対応規格のようなものがあるのかも知れません。


以上です。

改行をに変換するJinja2のカスタムフィルター

Google App Engine for PythonでテンプレートエンジンをDjangoからJinja2に変更したのですが、Djangoで使っていたlinebreaksbrフィルターが使えなくなり困ってしまいました。
そこでlinebreaksbrという同じフィルター名でJinja2のカスタムフィルターを作ってみました。

まず、jinja2_custom_filters.pyというファイルを作成して以下を記述します(ファイル名は適当につけました)。

def linebreaksbr(arg):
    return arg.replace("\n", "<br />\n")

このlinebreaksbrという関数を、Jinja2にカスタムフィルターとして登録します。

下のコードがスタートガイド(英語)で示されているJinja2を使うときの記述です。

import jinja2
jinja_environment = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
)

カスタムフィルターを登録するには、これ↑をこう↓します(下3行を追加)。

jinja_environment = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
)
jinja_environment.filters.update({
	"linebreaksbr": jinja2_custom_filters.linebreaksbr,
})

これで、テンプレートファイルで {{ variable|linebreaksbr }}とすると、改行が<br>に変換されるようになりました。

以下を参考にさせていただきました。

Google App Engine for Python でセッション管理

2011-03-25 追記ここから:

下のコードではDatastoreエンティティのkey値をセッションIDに使用していますが、これはセッションハイジャック脆弱性が生じてしまうので避けるべきだと気付きました。

key値は一見ランダムな文字列に見えますが、同じkindのエンティティのkey値は末尾が少し変わる程度なので、cookieに書き込んであるセッションIDから他人のセッションIDが類推できてしまいますね。

uuid.uuid4()等でランダムな値を生成する方法をとった方がよさそうです(下のコードはそのようには修正していません)

追記ここまで。


Google App Engine for Pythonで独自にセッション管理をするためのサンプルプログラムは検索したらいくつか見つかったのですが、ちょっと僕にはむずかしすぎるというか、何がどうなってそうなるのかサッパリわからなかったので、自分で理解できる範囲で書いてみました。

デキる人から見たらかなりアレだと思いますので、お気づきの点があったらぜひご指摘いただけるとうれしいです><

それと、HTMLのテンプレートまでここに書いたら長大なエントリーになってしまうので割愛してますが、なにかいい方法ないものでしょうか。どこかにまとめてアップロードすればいいのかな??

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os, re, datetime, hashlib

from google.appengine.ext import db
from google.appengine.api import memcache
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import run_wsgi_app

class User(db.Model):
# モデル(ユーザー)
    name = db.StringProperty()
    password = db.StringProperty()

class Session(db.Model):
# モデル(セッション)
    user = db.ReferenceProperty()

def getSession(request):
# クッキーのセッションIDとmemcache/Datastoreのセッション情報を照合する関数
# リクエストハンドラーのself.requestを引数として受け取る

    # クッキーからセッション番号を取得
    sid = request.cookies.get('SID', '')
    if sid:
        # まずmemcacheにそのセッション番号があるか探す
        session = memcache.get(sid)
        if session:
            return session

        # memcacheになければDatastoreから探す
        else:
            session = Session.get(sid)
            if session:
                # Datastoreにあればmemcacheにも入れておく
                memcache_key = str(session.key())
                memcache_value = {
                    'user' : {
                        'key' : str(session.user.key()),
                        'name' : session.user.name,
                    },
                }
                memcache.add(key=memcache_key, value=memcache_value, time=3600)

                return session


class ViewHome(webapp.RequestHandler):
# ホーム画面のView

    def get(self):
        # セッション情報を取得してテンプレート値としてセット
        template_values = {
            'session' : getSession(self.request),
        }
        path = os.path.join(os.path.dirname(__file__), 'templates/home.html')
        self.response.out.write(template.render(path, template_values))


class ViewSignup(webapp.RequestHandler):
# ユーザー登録画面のView

    def get(self):
        # セッション情報を取得してテンプレート値としてセット
        template_values = {
            'session' : getSession(self.request),
        }
        path = os.path.join(os.path.dirname(__file__), 'templates/signup.html')
        self.response.out.write(template.render(path, template_values))

    def post(self):
        # エラー表示の関数
        def viewError():
            template_values = {
                'error_messages' : error_messages,
                'error' : {
                    'name' : name,
                    'password' : password,
                }
            }

            path = os.path.join(os.path.dirname(__file__), 'templates/signup.html')
            self.response.out.write(template.render(path, template_values))

        # フォームからの値を受け取り
        name = self.request.get('name')
        password = self.request.get('password')

        # エラーメッセージのリスト
        error_messages = []

        # ユーザー名が半角6文字以上、32文字以下でなければエラーメッセージを設定
        if len(name) < 6 or len(name) > 32:
            error_messages.append('ユーザー名は6文字以上、32文字以下でお願いします。')

        # ユーザー名は半角英数・アンダースコアのみでなければエラーメッセージを設定
        match = re.search("([^\w])",name)
        if match:
            error_messages.append('ユーザー名は半角英数字と_(アンダースコア)でお願いします。')

        # パスワードは4文字以上、64文字以下でなければエラーメッセージを設定
        if len(password) < 4 or len(password) > 64:
            error_messages.append('パスワードは4文字以上、32文字以下でお願いします。')

        # パスワードは半角英数字のみでなければエラーメッセージを設定
        match = re.search("([^A-Za-z0-9])", password)
        if match:
            error_messages.append('パスワードは半角英数字でお願いします。')

        # ここまででエラーメッセージが1つでも設定されていたらエラー表示
        if error_messages:
            viewError()

        # ここまででエラーメッセージが1つもなければ...
        else:

            # 既に同じユーザー名が使用されていないかチェック
            user = User.all().filter('name = ', name).get()
            if user:
                error_messages.append('そのユーザー名はすでに使われています。')
                viewError()

            # 同じユーザー名が使用されていなければユーザー登録
            else:

                # パスワードをハッシュにする
                password = hashlib.sha1(password).hexdigest()

                # ユーザーを登録
                user = User(
                    name = name,
                    password = password,
                )
                user_key = user.put()

                template_values = {}
                path = os.path.join(os.path.dirname(__file__), 'templates/signup_done.html')
                self.response.out.write(template.render(path, template_values))


class ViewSignin(webapp.RequestHandler):
# ログイン画面のView

    def get(self):
        # セッション情報を取得してテンプレート値としてセット
        template_values = {
            'session' : getSession(self.request),
        }
        path = os.path.join(os.path.dirname(__file__), 'templates/signin.html')
        self.response.out.write(template.render(path, template_values))

    def post(self):

        # エラー表示の関数
        def viewError():
            template_values = {
                'error' : {
                    'name' : name,
                    'password' : password,
                }
            }

            path = os.path.join(os.path.dirname(__file__), 'templates/signin.html')
            self.response.out.write(template.render(path, template_values))

        # エラー判定用の変数(1ならエラーになる)
        error = 0;

        # フォームからの値を取得
        name = self.request.get('name')
        password = self.request.get('password')

        # ユーザー名が6文字未満か32文字より多ければエラー判定を設定
        if len(name) < 6 or len(name) > 32:
            error = 1

        # ユーザー名が半角英数とアンダースコア以外を含んでいたらエラー判定を設定
        match = re.search("([^\w])",name)
        if match:
            error = 1

        # パスワードが4文字未満か64文字より多ければエラー反映を設定
        if len(password) < 4 or len(password) > 64:
            error = 1

        # パスワードが半角英数以外を含んでいたらエラー判定を設定
        match = re.search("([^A-Za-z0-9])", password)
        if match:
            error = 1

        # ここまででエラー判定が設定されていたらエラー表示
        if error:

            # エラー表示
            viewError()

        # ここまででエラー判定が設定されていなければ...
        else:

            # 登録済みユーザーかどうかチェック
            user = User.all().filter('name = ', name).get()

            # 未登録ユーザーならエラー表示
            if not user:
                viewError()

            # 登録済みユーザーなら...
            else:

                # パスワードのハッシュを作成
                password_hashed = hashlib.sha1(password).hexdigest()

                # Datastorに登録されているパスワードのハッシュが一致したら
                if password_hashed == user.password:

                    # Datastoreにセッション情報を保存
                    session_key = Session(
                        user = user,
                    ).put()

                    # memcacheにセッション情報を保存
                    memcache_key = str(session_key)
                    memcache_value = {
                        'user' : {
                            'key' : str(user.key()),
                            'name' : user.name,
                        },
                    }
                    memcache.add(key=memcache_key, value=memcache_value, time=3600)

                    # クッキーをセット
                    expires_date = datetime.datetime.utcnow() + datetime.timedelta(365)
                    expires = expires_date.strftime("%d %b %Y %H:%M:%S GMT")
                    self.response.headers.add_header(
                       'Set-Cookie',
                       'SID=' + str(session_key) + '; expires=' + expires
                    )

                    # ホームに飛ばす
                    self.redirect('/')

                else:
                    # エラー表示
                    viewError()

class ViewSignout(webapp.RequestHandler):
# ログアウト処理

    def get(self):
        template_values = {}

        sid = self.request.cookies.get('SID', '')

        # memcacheのクリア
        memcache.delete(sid)

        # DataStoreエンティティの削除
        db.delete(sid)

        # cookieの削除
        self.response.headers.add_header(
            'Set-Cookie',
            'SID=0;expires=Fri, 01-Jan-1950 00:00:00 GMT'
        )

        # ホームに飛ばす
        self.redirect('/')


class ViewPage1(webapp.RequestHandler):
# ページ1画面のView

    def get(self):
        # セッション情報を取得してテンプレート値としてセット
        template_values = {
            'session' : getSession(self.request),
        }
        path = os.path.join(os.path.dirname(__file__), 'templates/page1.html')
        self.response.out.write(template.render(path, template_values))


class ViewPage2(webapp.RequestHandler):
# ページ2画面のView

    def get(self):
        # セッション情報を取得してテンプレート値としてセット
        template_values = {
            'session' : getSession(self.request),
        }
        path = os.path.join(os.path.dirname(__file__), 'templates/page2.html')
        self.response.out.write(template.render(path, template_values))


# URLとViewの対応
application = webapp.WSGIApplication([
    ('/page2/?$', ViewPage2),
    ('/page1/?$', ViewPage1),
    ('/signout/?$', ViewSignout),
    ('/signin/?$', ViewSignin),
    ('/signup/?$', ViewSignup),
    ('/$', ViewHome),
],debug=True)


def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()

Google App Engine/Python でタギングを実現する方法

Google App Engine で、いわゆるタギングtagging )を実現したいのですが、うまい方法がわからず悩んでいます。

いまは以下のようにしています。

class Tag(db.Model):
    label = db.StringProperty()

class Item(db.Model):
    name = db.StringProperty()
    tags_key = db.ListProperty() # リストの要素は Tag のエンティティの key 値。
    def getTags(self):
        tags = []
        for tag_key in self.tags_key:
            label = Tag.get(tag_key).label
            tag = {
                'label' : label,
                'url' : urllib.quote(label.encode('utf-8')),
            }
            tags.append(tag)
        return tags

このようにしておいて、Django テンプレートの方で以下のようにしてタグを取り出しています。

<ul class="tags">
{% for tag in item.getTags %}
<li><a href="/tag/{{ tag.url }}">{{ tag.label }}</a></li>
{% endfor %}
</ul>

ただ、ある特定のタグがつけられた Item のエンティティーを取り出すのが以下のようになっていて、若干ややこしいです。

# '/tags/([a-fA-F0-9+%]+)$' のリクエストハンドラー
class ViewTag(webapp.RequestHandler):
    def get(self, tag_url):
        # URL の中の URL エンコードされたタグのラベルから Tag のエンティティを取得する。
        query = Tag.all()
        query.filter('label = ', tag_url.decode('utf-8'))
        tag = query.get()
        # 取得した Tag のエンティティの key 値 を tags_key プロパティに含む Item のエンティティを取得する。
        query = Item.all()
        query.filter('tags = ', tag.key())
        items = query.fetch(50)
        template_values['items'] = items
        ......

なにかもっとよい方法がないものかと思っているのですが、アドバイスいただけると幸いです。