"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
It's fragile to hardcode the site IDs like that, in case they change. The cleaner way of accomplishing the same thing is to check the current site's domain:
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 でよく使われる get_absolute_url() は、オブジェクトの URL をドメイン名なしで取得するのに便利ですが、場合によってはオブジェクトの完全な URL(https:// やドメイン名を含む URL)を表示したいこともあります。その場合は、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'
>>> "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()の値に設定します。
A RequestSite object has a similar
interface to a normal Site object,
except its __init__()
method takes an HttpRequest object. It's able to deduce
the domain and name by looking at the request's domain. It has
save() and delete() methods to match the interface of
Site, but the methods raise
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には適用されず、常に変更されていないホストが使用されます。