“sites” フレームワーク

revision-up-to:11321 (1.1)

Django にはオプションとして使える “sites” フレームワークが付属しています。 “sites” はオブジェクトや機能を特定の Web サイトに関連付けるためのフックであ ると同時に、 Django で作成したサイトのドメイン名と「分かりやすい名前 verbose name」を保存しています。

一つの Django で複数のサイトを管理していて、サイト間に違いを持たせたい場合 に使ってください。

sites フレームワークは、簡単なモデルだけでできています:

class django.contrib.sites.models.Site

モデルには、 domainname という二つのフィールドがあり ます。あるサイトの設定ファイルの SITE_ID 設定は、そのサイトを表 す Site オブジェクトのデータベース上 での ID を指定します。

site の使い道は自由ですが、 Django では簡単な呼び出し規約で自動的に sites を使える方法を 2 種類提供しています。

使用例

どういう状況で sites を使うのでしょうか?例を挙げて説明しましょう。

コンテンツを複数のサイトに関連づける

LJWorld.comLawrence.com は同じニュース組織、Kansaz 州 Lawrence にある Lawrence Journal-World newspaper が管理しています。 LJWorld.com はニュース に、 Lawrence.com は地域の娯楽情報にフォーカスしていますが、編集者は 両方の サイトで同じニュースを公開したいと考える場合もあります。

この問題を何も考えずに扱うなら、サイト構築担当者に対して、 LJWorld.com と Lawrence.com に同じ記事を出すよう 2 度依頼することになります。しかしこれは サイト構築担当にとって非効率的ですし、同じ記事のコピーが複数データベースに 入ることになってしまいます。

もっとましで簡単な方法ががあります: どちらのサイトも同じデータベースを使い、 1 つの記事を複数のサイトに関連づけるというものです。この関係は、 Django モ デルの用語で言えば Article モデルの ManyToManyField で表現されます:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    sites = models.ManyToManyField(Site)

このモデルは、以下のようにいくつかの点で優れています:

  • site 構築担当が 1 つのインタフェース (Django admin サイト) で両方のサ イトにある全てのコンテンツを編集できます。

  • 同じ記事をデータベース上に何度も書き込まなくて済み、一つのレコードと して保存できます。

  • サイト開発者は同じビューコードを両方のサイトで使えます。ビューコード では、リクエストされている記事が現在のサイト用のものかチェックします。 例えば以下のようなコードになるでしょう:

    from django.conf import settings
    
    def article_detail(request, article_id):
        try:
            a = Article.objects.get(id=article_id, sites__id__exact=settings.SITE_ID)
        except Article.DoesNotExist:
            raise Http404
        # ...
    

コンテンツを単一のサイトに関連づける

あるモデルを ForeignKey を使って 他対一の関係で Site に関連づけても構 いません。

例えば、ある記事をあるサイトだけで表示できるようにしたければ、モデルは以下 のように書きます:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site)

この方法でも、前節で述べたのと同じような恩恵を得られます。

ビュー内で現在のサイトをフックに使う

低水準では、 Django ビューの中で sites フレームワークを使って、ビューを呼び 出しているサイトごとに固有の処理を実行できます。

例えば:

from django.conf import settings

def my_view(request):
    if settings.SITE_ID == 3:
        # Do something.
    else:
        # Do something else.

もちろん、上のようにサイト ID をハードコードするのは見栄えよくありません。 この種のハードコード化は、すぐに実行しなければならないハックのときにはベス トでしょう。同じことをよりクリーンに実現するには、サイトのドメイン名をチェッ クします:

from django.conf import settings
from django.contrib.sites.models import Site

def my_view(request):
    current_site = Site.objects.get(id=settings.SITE_ID)
    if current_site.domain == 'foo.com':
        # Do something
    else:
        # Do something else.

settings.SITE_ID の値から Site オブジェクトを取得するというイディ オムはよく使われるので、 Site のモデ ルマネジャには get_current() メソッドがあります。以下の例は上の例と同じ です:

from django.contrib.sites.models import Site

def my_view(request):
    current_site = Site.objects.get_current()
    if current_site.domain == 'foo.com':
        # Do something
    else:
        # Do something else.

現在のドメインを取得して表示する

LJWorld.com と Lawrence.com には e-mail による通知機能があり、読者が登録し ておくとニュースが届いたときに通知メールを受け取れるようになっています。こ のからくりは簡単で、 Web フォームから登録すると、「ご登録ありがとうございま した」という内容のメールを受け取ります。

登録処理を行うコードを二度開発するのは非効率的で冗長なので、舞台裏では複数 サイトで同じコードを使うようにします。ただし、「ご登録ありがとうございます」 通知はサイトごとに変えなければなりません。 Site オブジェクトを使えば、現在のサイ トの namedomain の値を使って「ありがとうございます」メッセージ を抽象化できます。

フォーム処理ビューの例は以下の通りです:

from django.contrib.sites.models import Site
from django.core.mail import send_mail

def register_for_newsletter(request):
    # フォームの値チェックなどを行い、ユーザを登録する
    # ...

    current_site = Site.objects.get_current()
    send_mail('Thanks for subscribing to %s alerts' % current_site.name,
        'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
        'editor@%s' % current_site.domain,
        [user.email])

    # ...

Lawrence.com では、 e-mail の件名は “Thanks for subscribing to lawrence.com alerts.” です。 LJWorld.com では、件名は “Thanks for subscribing to LJWorld.com alerts.” で、メール本体も同様です。

より柔軟 (で、重たい) 方法として、 Django テンプレートシステムを使った方法 と比較してみましょう。 Lawrence.com と LJWorld.com は別々のテンプレートディ レクトリ (TEMPLATE_DIRS) を持っているので、以下のようにすればサ イト間の違いをテンプレートシステムに追い出せます:

from django.core.mail import send_mail
from django.template import loader, Context

def register_for_newsletter(request):
    # フォームの値チェックなどを行い、ユーザを登録する
    # ...

    subject = loader.get_template('alerts/subject.txt').render(Context({}))
    message = loader.get_template('alerts/message.txt').render(Context({}))
    send_mail(subject, message, 'editor@ljworld.com', [user.email])

    # ...

この場合、 LJWorld.com と Lawrence.com のテンプレートディレクトリ下に subject.txtmessage.txt の二つのテンプレートを作成します。 ただし、こうすればより柔軟にはなりますが、複雑さは増します。

不要な複雑さと冗長性を排除するためには、 Site オブジェクトを可能な限り使うとよ いでしょう。

現在のドメインの完全な URL を取得する

Django の get_absolute_url() は、オブジェクトの URL をドメイン名なしで 取得する際には便利ですが、場合によっては、完全な URL、すなわち http:// とドメイン名、その他全てを表示したいこともあるでしょう。これには sites フレー ムワークを使います。簡単な例を示します:

>>> from django.contrib.sites.models import Site
>>> obj = MyModel.objects.get(id=3)
>>> obj.get_absolute_url()
'/mymodel/objects/3/'
>>> Site.objects.get_current().domain
'example.com'
>>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'

現在の Site オブジェクトをキャッシュする

現在のサイトを表す Site オブジェクト は、もともとデータベースに保存されているので、 get_current() を呼ぶたび にデータベース呼び出しが発生してしまいます。ただし、 Django はもう少し賢く Site を扱います。 すなわち、現在のサイトをキャッシュし、それ以降 get_current() を呼び出し ても、データベースに触らずキャッシュから Site オブジェクトを返します。

何らかの理由で、データベースクエリを強制したい場合には、 clear_cache()`() を使って キャッシュを消去させられます:

# 最初の呼び出し: データベースから Site オブジェクトを取り出す
current_site = Site.objects.get_current()
# ...

# 2 回目の呼び出し: キャッシュから取り出す
current_site = Site.objects.get_current()
# ...

# 3 回目の呼び出しで、データベースクエリを強制する
Site.objects.clear_cache()
current_site = Site.objects.get_current()

CurrentSiteManager

class django.contrib.sites.managers.CurrentSiteManager

アプリケーション内で Site が重要な働 きを持っている場合、 CurrentSiteManager という便利なクラ スを検討してみてください。

CurrentSiteManager はモデルの マネジャ クラスで、クエリ結果を現在の Site に関連づけられたオブジェクトだけ に自動的にフィルタします。

CurrentSiteManager を使うには、モデ ルの中に明示的に追加します。例を示します:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()

このモデルでは、 Photo.objects.all() を使うとデーターベース上の全ての Photo オブジェクトを返しますが、 Photo.on_site.all() を使うと、 SITE_ID 設定に従って、現在のサイトに関連づけられた Photo オ ブジェクトだけを返します。

つまり、以下の二つの文は等価になります:

Photo.objects.filter(site=settings.SITE_ID)
Photo.on_site.all()

CurrentSiteManagerPhoto の どのフィールドが Site なのかをどうやっ て見付けるのでしょう? CurrentSiteManager はデフォルトでは site という名前のフィールドを探します。モデル内で site 以外の 名前の ForeignKeyManyToManyField` を定義している場 合、 CurrentSiteManager に明示的に フィールド名を渡す必要があります。以下のモデルでは、 publish_on という 名前のフィールドがその例です:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    publish_on = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager('publish_on')

CurrentSiteManager を使って、存在し ないフィールド名を渡そうとすると、 Django は ValueError を送出します。

CurrentSiteManager を使っていたとし ても、結局は (サイト固有でない) 通常の Manager をモデル内に持っておく必 要に迫られるでしょう。 マネジャのドキュメント でも説明しましたが、手動でマネジャを定義すると、Django は objects = models.Manager() を使った自動マネジャ生成を行わなくなるからで す。また、 Django の一部 (例えば admin サイトや汎用ビュー) では、モデル内で 最初に定義されている マネジャを常に使うようになっています。そのため、 admin サイトから全てのオブジェクトにアクセスしたい (サイト固有のフィルタリ ングを行わない) 場合には、モデル内で objects = models.Manager()CurrentSiteManager の定義より前 に配置しておかねばなりません。

Django 内での site フレームワークの役割

Django を使う際、必ずしも sites フレームワークを使わねばならないというわけ ではありません。とはいえ、 Django はいくつかの場所で sites の恩恵を利用でき るので、ぜひとも sites を活用するよう勧めます。 Django を単一のサイトを駆動 するためだけに使っているとしても、ひと手間かけてサイトオブジェクトを作成し、 domainname を指定して、その ID を SITE_ID 設定に指定 してみてください。

Django における sites フレームワークの役割は以下の通りです:

  • リダイレクションフレームワーク では、各 リダイレクトオブジェクトが特定のサイトに関連づけられています。 Django が リダイレクト先を探すとき、フ レームワークは現在の SITE_ID を考慮します。
  • コメントフレームワークでは、各コメントが特定のサイトに関連づけられて います。コメントが投稿されると、 site は現在の SITE_ID に 設定されます。コメントを何らかのテンプレートタグでリスト表示する場合、現 在のサイトに関するコメントだけが表示されます。
  • フラットページフレームワーク では、各フ ラットページは特定のサイトに関連づけられています。フラットページを作成す る際には、 site を指定せねばなりません。 FlatpageFallbackMiddleware は現在の SITE_ID をチェックして、表示すべきフラットページを取 得します。
  • 配信フレームワーク では、 title および description のテンプレートから自動的に {{ site }} にアクセ スできます。 {{ site }}Site オブジェクトで、現在の site を表現します。また、フィード項目の URL を提供 するためのフックでは、完全指定 (full-qualified) のドメインを指定していな い場合、現在の Site オブジェクトの domain を使います。
  • 認証フレームワーク では、 django.contrib.auth.views.login() ビューが現在の Site 名を {{ site_name }} とい う形でテンプレートに渡しています。
  • ショートカットビュー (django.views.defaults.shortcut()) では、オブ ジェクトの URL のドメイン部を計算する際に現在の Site オブジェクトを使います。
  • Site オブジェクトに完全指定のホスト 名を定義しておくと、 admin フレームワークで、現在の サイト上で表示 (view on site) のリンク URL の構築に使います。

RequestSite オブジェクト

django.contrib 内のアプリケーションの中には、 sites フレームワークを利用はできるけれども、 必須にはしない ようなものが あります (sites を使いたくない人や、 sites フレームワークが必要とするデータ ベーステーブルの作成が 不可能な 人もいるためです)。こうした場合のために、 sites フレームワークでは RequestSite クラスを提供しています。このクラスは、データベースバックエンド上に sites フ レームワークがない場合のフォールバックとして使われます。

RequestSite オブジェクトは、通常の Site オブジェクトと同様のインタフェー スを備えていますが、 __init__()HttpRequest オブジェクトを取るところが違います。この オブジェクトはリクエストのドメイン情報を見ることで domainname を決定します。 Site オブジェクトとイ ンタフェースを合わせるため、 RequestSite オブジェクトにも save()delete() といったメソッドが ありますが、これらのメソッドを呼び出すと NotImplementedError を送出 します。