# Django 5.2.9 ドキュメント

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

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

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

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` オブジェクトを識別するために `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` には適用されず、常に変更されていないホストが使用されます。