"sites" フレームワーク¶
Django にはオプションで "sites" フレームワークが付属しています。これは、オブジェクトや機能を特定の Web サイトに関連付けるためのフックで、 Django で動くサイトのドメイン名や "冗長な" 名前を保持する場所です。
1 つの Django インストールで複数のサイトを運用し、これらのサイトを何らかの方法で区別する必要がある場合に使用します。
sites フレームワークは、主にこのモデルに基づいています:
-
class
models.
Site
¶ ウェブサイトの
domain
とname
属性を格納するモデル。-
domain
¶ ウェブサイトに関連付けられた完全修飾ドメイン名。たとえば、
www.example.com
。
-
name
¶ 人間が読める "冗長な" ウェブサイトの名前。
-
SITE_ID
設定は、その特定の設定ファイルに関連付けられている Site
オブジェクトのデータベース ID を指定します。この設定が省略された場合、 get_current_site()
関数は、domain
と request.get_host()
メソッドからのホスト名を比較することで、現在のサイトを取得しようとします。
これをどのように使用するかはあなた次第ですが、Djangoはいくつかの慣習を通じて自動的にいくつかの方法で使用します。
使用例¶
なぜ sites を使うのでしょうか? それは例を挙げるとわかりやすいでしょう。
複数のサイトとコンテンツを関連付ける¶
LJWorld.com と Lawrence.com のサイトは、同じニュース組織、カンザス州ローレンスにあるローレンス・ジャーナル・ワールド新聞社によって運営されていました。LJWorld.com はニュースに焦点を当てていましたが、Lawrence.com は地元のエンターテインメントに焦点を当てていました。しかし、時には編集者たちは記事を 両方の サイトに掲載したいと思うことがありました。
問題を解決する素朴な方法は、サイトのプロデューサーに同じストーリーを二度公開させることです。一度は LJWorld.com 用に、もう一度は Lawrence.com 用です。しかし、それはサイトプロデューサーにとって非効率的であり、データベースに同じストーリーの複数のコピーを保存することは冗長です。
より良い解決方法は、内容の重複を解消することです。どちらのサイトも同じ記事データベースを使用し、記事は1つ以上のサイトに関連付けられます。Djangoモデルの用語では、これは Article
モデル中に ManyToManyField
で表されます:
from django.contrib.sites.models import Site
from django.db import models
class Article(models.Model):
headline = models.CharField(max_length=200)
# ...
sites = models.ManyToManyField(Site)
これにより、いくつかのことが非常にうまく達成されます:
サイト制作者が両方のサイトの全てのコンテンツをシングルインターフェイス(Django 管理画面)で編集できるようにします。
同じストーリーをデータベースに2回公開する必要はありません。データベースには単一のレコードのみが存在します。
これにより、サイト開発者は両方のサイトで同じ Django ビューコードを使うことができます。指定されたストーリーを表示するビューコードは、リクエストされたストーリーが 現在のサイトにあるかどうかをチェックします。以下のようになります:
from django.contrib.sites.shortcuts import get_current_site def article_detail(request, article_id): try: a = Article.objects.get(id=article_id, sites__id=get_current_site(request).id) except Article.DoesNotExist: raise Http404("Article does not exist on this site") # ...
1つのサイトとコンテンツを関連付ける¶
同様に、ForeignKey
を使って、あるモデルを Site
モデルに多対一のリレーションシップで関連付けることができます。
たとえば、1つのサイトにのみ記事を許可する場合は、次のようなモデルを使用します:
from django.contrib.sites.models import Site
from django.db import models
class Article(models.Model):
headline = models.CharField(max_length=200)
# ...
site = models.ForeignKey(Site, on_delete=models.CASCADE)
この利点は前のセクションで説明されたものと同じです。
ビューから現在のサイトにフックする¶
Djangoのビュー内でサイトフレームワークを利用して、ビューが呼び出されているサイトに基づいた特定の処理を行うことができます。たとえば:
from django.conf import settings
def my_view(request):
if settings.SITE_ID == 3:
# Do something.
pass
else:
# Do something else.
pass
そのようにサイトIDをハードコードするのは、それらが変更された場合に脆弱です。同じことを達成するよりクリーンな方法は、現在のサイトのドメインを確認することです:
from django.contrib.sites.shortcuts import get_current_site
def my_view(request):
current_site = get_current_site(request)
if current_site.domain == "foo.com":
# Do something
pass
else:
# Do something else.
pass
これには、サイトフレームワークがインストールされているかどうかを確認するという利点もあります。インストールされていない場合は、 RequestSite
インスタンスを返します。
リクエストオブジェクトにアクセスできない場合は、Site
モデルのマネージャの get_current()
メソッドを使用できます。その際、設定ファイルに SITE_ID
の設定が含まれていることを確認してください。この例は、前の例と同等です:
from django.contrib.sites.models import Site
def my_function_without_request():
current_site = Site.objects.get_current()
if current_site.domain == "foo.com":
# Do something
pass
else:
# Do something else.
pass
表示用の現在のドメインを取得する¶
LJWorld.com と Lawrence.com はどちらも、ニュースが発生したときに通知を受け取るために読者がサインアップできるメールアラート機能を持っています。とても基本的なものです。読者がウェブフォームでサインアップをすると、すぐに「ご登録ありがとうございます」というメールが送られます。
このサインアップ処理コードを2回実装するのは、非効率的で冗長です。そこで、サイトは裏で同じコードを使用しています。しかし、各サイトの「サインアップしていただきありがとうございます」通知は異なる必要があります。 Site
オブジェクトを使用することで、現在のサイトの name
と domain
の値を使用して「ありがとうございます」通知を抽象化できます。
以下は、フォームハンドリングビューの例です:
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
def register_for_newsletter(request):
# Check form values, etc., and subscribe the user.
# ...
current_site = get_current_site(request)
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 では、このメールの件名は "Thanks for subscribing to lawrence.com alerts." です。LJWorld.com では、メールの件名は "Thanks for subscribing to LJWorld.com alerts." となります。メールのメッセージ本体も同様です。
これを行うさらに柔軟(しかしより重い)方法として、Djangoのテンプレートシステムを使用できます。Lawrence.com と LJWorld.com が異なるテンプレートディレクトリ(DIRS
)を持っていると仮定すると、以下のようにテンプレートシステムに任せることができます:
from django.core.mail import send_mail
from django.template import loader
def register_for_newsletter(request):
# Check form values, etc., and subscribe the user.
# ...
subject = loader.get_template("alerts/subject.txt").render({})
message = loader.get_template("alerts/message.txt").render({})
send_mail(subject, message, "editor@ljworld.com", [user.email])
# ...
この場合、LJWorld.com と Lawrence.com のテンプレートディレクトリの両方について、subject.txt
と message.txt
のテンプレートファイルを作成する必要があります。これにより柔軟性は高まりますが、同時により複雑になります。
Site
クラスのオブジェクトを可能な限り活用することは、不要な複雑さと冗長性を排除する良い考えです。
フルURLのための現在のドメインの取得¶
Django's get_absolute_url()
convention is nice for getting your objects'
URL without the domain name, but in some cases you might want to display the
full URL -- with https://
and the domain and everything -- for an object.
To do this, you can use the sites framework. An example:
>>> 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'
>>> "https://%s%s" % (Site.objects.get_current().domain, obj.get_absolute_url())
'https://example.com/mymodel/objects/3/'
サイトフレームワークを有効にする¶
サイトフレームワークを有効にするには、次の手順に従ってください:
django.contrib.sites'
をINSTALLED_APPS
設定に追加します。SITE_ID
設定を定義します:SITE_ID = 1
migrate
を実行します。
django.contrib.sites
は、デフォルトサイトとして名前とドメインが example.com
であるサイトを作成する post_migrate
シグナルハンドラを登録します。このサイトは、Djangoがテストデータベースを作成した後にも作成されます。プロジェクトの正しい名前とドメインを設定するには、データマイグレーション を使用できます。
本番環境で異なるサイトを提供するには、各 SITE_ID
用に個別の設定ファイルを作成します(共有設定の重複を避けるために、共通の設定ファイルからインポートすることもできます)。そして、各サイトに適切な DJANGO_SETTINGS_MODULE
を指定します。
現在の Site
オブジェクトをキャッシュする¶
現在のサイトはデータベースに保存されているため、Site.objects.get_current()
を呼び出すたびにデータベースクエリが発生する可能性があります。しかし、Djangoは少し賢く: 最初のリクエストで、現在のサイトがキャッシュされ、以降の呼び出しではデータベースにアクセスする代わりにキャッシュされたデータが返されます。
何らかの理由でデータベースクエリを強制的に実行したい場合は、Django にキャッシュをクリアするよう指示できます。これを行うには、 Site.objects.clear_cache()
を使用します。
# First call; current site fetched from database.
current_site = Site.objects.get_current()
# ...
# Second call; current site fetched from cache.
current_site = Site.objects.get_current()
# ...
# Force a database query for the third call.
Site.objects.clear_cache()
current_site = Site.objects.get_current()
CurrentSiteManager
¶
-
class
managers.
CurrentSiteManager
¶
もし Site
があなたのアプリケーションで重要な役割を果たしているなら、モデルに CurrentSiteManager
を使うことを検討してください。これは マネージャー であり、そのクエリが自動的に現在の Site
に関連付けられたオブジェクトのみを含むようにフィルタリングされます。
CurrentSiteManager
を明示的にモデルに追加することで使用します。例えば:
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
from django.db import models
class Photo(models.Model):
photo = models.FileField(upload_to="photos")
photographer_name = models.CharField(max_length=100)
pub_date = models.DateField()
site = models.ForeignKey(Site, on_delete=models.CASCADE)
objects = models.Manager()
on_site = CurrentSiteManager()
このモデルを使うと、Photo.objects.all()
はデータベース内の全ての Photo
オブジェクトを返しますが、Photo.on_site.all()
は SITE_ID
設定に従って、現在のサイトに関連付けられた Photo
オブジェクトのみを返します。
別の言い方をすると、これら2つのステートメントは等価です:
Photo.objects.filter(site=settings.SITE_ID)
Photo.on_site.all()
CurrentSiteManager
は、どうやって Photo
のどのフィールドが Site
であるかを知ったのでしょうか?デフォルトでは、CurrentSiteManager
は、site
という名前の ForeignKey
または sites
という名前の ManyToManyField
をフィルタに使用するフィールドを探します。もし、モデルが関連付けられている Site
オブジェクトを識別するために site
や sites
以外の名前のフィールドを使用する場合は、カスタムフィールド名をパラメータとして CurrentSiteManager
に明示的に渡す必要があります。以下のモデル例は、publish_on
というフィールドを持っています:
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
from django.db import models
class Photo(models.Model):
photo = models.FileField(upload_to="photos")
photographer_name = models.CharField(max_length=100)
pub_date = models.DateField()
publish_on = models.ForeignKey(Site, on_delete=models.CASCADE)
objects = models.Manager()
on_site = CurrentSiteManager("publish_on")
CurrentSiteManager
を使用し、存在しないフィールド名を渡した場合、Django は ValueError
を発生させます。
最後に、 CurrentSiteManager
を使用しても、モデルに通常の(サイト固有ではない) Manager
を保持したい場合があることに注意してください。 マネージャのドキュメント で説明されているように、マネージャを手動で定義すると、Djangoは自動的な objects = models.Manager()
マネージャを作成しません。また、Djangoの特定の部分、特にDjango管理サイトとジェネリックビューは、モデル内で 最初に 定義されたマネージャを使用するため、管理サイトが全てのオブジェクト(サイト固有のものだけでなく)にアクセスできるようにするには、 CurrentSiteManager
を定義する前に、モデルに objects = models.Manager()
を置いてください。
Site ミドルウェア¶
もしこのパターンをよく使うなら、:
from django.contrib.sites.models import Site
def my_view(request):
site = Site.objects.get_current()
...
繰り返しを避けるために、 MIDDLEWARE
に django.contrib.sites.middleware.CurrentSiteMiddleware
を追加してください。このミドルウェアは、リクエストオブジェクトのすべてに site
属性を設定するため、request.site
を使って現在のサイトを取得できます。
Django はどのように sites フレームワークを使うか¶
sites フレームワークの利用は必須ではありませんが、Django はいくつかの場所でこれを利用しているので、強く推奨します。Django のインストールが 1 つのサイトだけであっても、2 秒かけて domain
と name
でサイトオブジェクトを作成し、 SITE_ID
設定でその ID を指定してください。
Django は、sites フレームワークを次のように使用します:
redirects フレームワーク
では、それぞれのリダイレクトオブジェクトが特定のサイトに関連付けられています。Django がリダイレクトを検索する際には、現在のサイトも考慮されます。flatpages フレームワーク
で、各フラットページは特定のサイトに関連付けられています。フラットページを作成するときには、そのSite
を指定し、FlatpageFallbackMiddleware
は表示するフラットページを取得する際に現在のサイトをチェックします。syndication フレームワーク
では、title
とdescription
のテンプレートは、現在のサイトを表すSite
オブジェクトを表す変数{{ site }}
に自動的にアクセスできます。また、アイテムの URL を提供するためのフックは、完全修飾ドメインを指定しない場合、現在のSite
オブジェクトのdomain
を使用します。認証フレームワーク
では、django.contrib.auth.views.LoginView
は現在のSite
名をテンプレートに{{ site_name }}
として渡します。- ショートカットビュー(
django.contrib.contenttypes.views.shortcut
) は、オブジェクトのURLを計算する際、現在のSite
オブジェクトのドメインを使用します。 - admin フレームワークでは、 "view on site" リンクは、リダイレクト先のサイトのドメインを求めるために、現在の
Site
を使用します。
RequestSite
オブジェクト¶
一部の django.contrib アプリケーションはサイトフレームワークを活用していますが、サイトフレームワークをデータベースにインストールすることを 必要としない ように設計されています。(サイトフレームワークが要求する追加のデータベーステーブルをインストールしたくない、またはインストールできない人もいます。) そのような場合のために、フレームワークは django.contrib.sites.requests.RequestSite
クラスを提供しており、データベースをバックエンドとするサイトフレームワークが利用できないときにフォールバックとして使用できます。
-
class
requests.
RequestSite
¶ Site
の主要なインターフェイス(つまり、domain
とname
属性を持つ)を共有するクラスですが、データをデータベースではなく Django のHttpRequest
オブジェクトから取得します。-
__init__
(request)¶ name
およびdomain
属性をget_host()
の値に設定します。
-
RequestSite
オブジェクトは、通常の Site
オブジェクトに似たインターフェースを持っていますが、その __init__()
メソッドは HttpRequest
オブジェクトを引数に取ります。リクエストのドメインを見て、domain
と name
を推測できます。 Site
のインターフェースに合わせた save()
および delete()
メソッドを持っていますが、これらのメソッドは NotImplementedError
を発生させます。
get_current_site
ショートカット¶
最後に、繰り返しのフォールバックコードを避けるために、フレームワークは django.contrib.sites.shortcuts.get_current_site()
関数を提供しています。
-
shortcuts.
get_current_site
(request)¶ django.contrib.sites
がインストールされているかをチェックし、リクエストに基づいて現在のSite
オブジェクトまたはRequestSite
オブジェクトのどちらかを返す関数です。SITE_ID
設定が定義されていない場合、request.get_host()
を使用して現在のサイトを検索します。Hostヘッダーにポートが明示的に指定されている場合 (例:
example.com:80
)、request.get_host()
によってドメインとポートの両方が返されることがあります。そのような場合、ホストがデータベースのレコードと一致しないために検索が失敗したら、ポートは取り除かれ、ドメイン部分のみで検索が再試行されます。この処理はRequestSite
には適用されず、常に変更されていないホストが使用されます。