"sites" フレームワーク

Django にはオプションで "sites" フレームワークが付属しています。これは、オブジェクトや機能を特定の Web サイトに関連付けるためのフックで、 Django で動くサイトのドメイン名や "冗長な" 名前を保持する場所です。

1 つの Django インストールで複数のサイトを運用し、これらのサイトを何らかの方法で区別する必要がある場合に使用します。

sites フレームワークは、主にこのモデルに基づいています:

class models.Site

ウェブサイトの domainname 属性を格納するモデル。

domain

ウェブサイトに関連付けられた完全修飾ドメイン名。たとえば、 www.example.com

name

人間が読める "冗長な" ウェブサイトの名前。

SITE_ID 設定は、その特定の設定ファイルに関連付けられている Site オブジェクトのデータベース ID を指定します。この設定が省略された場合、 get_current_site() 関数は、domainrequest.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 オブジェクトを使用することで、現在のサイトの namedomain の値を使用して「ありがとうございます」通知を抽象化できます。

以下は、フォームハンドリングビューの例です:

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.txtmessage.txt のテンプレートファイルを作成する必要があります。これにより柔軟性は高まりますが、同時により複雑になります。

Site クラスのオブジェクトを可能な限り活用することは、不要な複雑さと冗長性を排除する良い考えです。

フルURLのための現在のドメインの取得

Django の get_absolute_url() の慣習は、オブジェクトのURLをドメイン名なしで取得するのに便利ですが、場合によってはオブジェクトの完全な URL (http:// やドメイン名など全てを含む) を表示したいこともあるでしょう。これを行うには、サイトフレームワークを使用できます。例:

>>> 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/'

サイトフレームワークを有効にする

サイトフレームワークを有効にするには、次の手順に従ってください:

  1. django.contrib.sites'INSTALLED_APPS 設定に追加します。

  2. SITE_ID 設定を定義します:

    SITE_ID = 1
    
  3. 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 に関連付けられたオブジェクトのみを含むようにフィルタリングされます。

必須 SITE_ID

CurrentSiteManager は、設定ファイルに SITE_ID 設定が定義されている場合にのみ使用可能です。

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 オブジェクトを識別するために sitesites 以外の名前のフィールドを使用する場合は、カスタムフィールド名をパラメータとして 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()
    ...

繰り返しを避けるために、 MIDDLEWAREdjango.contrib.sites.middleware.CurrentSiteMiddleware を追加してください。このミドルウェアは、リクエストオブジェクトのすべてに site 属性を設定するため、request.site を使って現在のサイトを取得できます。

Django はどのように sites フレームワークを使うか

sites フレームワークの利用は必須ではありませんが、Django はいくつかの場所でこれを利用しているので、強く推奨します。Django のインストールが 1 つのサイトだけであっても、2 秒かけて domainname でサイトオブジェクトを作成し、 SITE_ID 設定でその ID を指定してください。

Django は、sites フレームワークを次のように使用します:

  • redirects フレームワーク では、それぞれのリダイレクトオブジェクトが特定のサイトに関連付けられています。Django がリダイレクトを検索する際には、現在のサイトも考慮されます。
  • flatpages フレームワーク で、各フラットページは特定のサイトに関連付けられています。フラットページを作成するときには、その Site を指定し、 FlatpageFallbackMiddleware は表示するフラットページを取得する際に現在のサイトをチェックします。
  • syndication フレームワーク では、titledescription のテンプレートは、現在のサイトを表す 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 の主要なインターフェイス(つまり、domainname 属性を持つ)を共有するクラスですが、データをデータベースではなく Django の HttpRequest オブジェクトから取得します。

__init__(request)

name および domain 属性を get_host() の値に設定します。

RequestSite オブジェクトは、通常の Site オブジェクトに似たインターフェースを持っていますが、その __init__() メソッドは HttpRequest オブジェクトを引数に取ります。リクエストのドメインを見て、domainname を推測できます。 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 には適用されず、常に変更されていないホストが使用されます。

Back to Top