「網站」框架

Django 附帶一個可選的「網站」框架。它是一個用於將物件和功能關聯到特定網站的鉤子,並且是您以 Django 驅動的網站的網域名稱和「詳細」名稱的存放位置。

如果您的單一 Django 安裝驅動多個網站,並且您需要以某種方式區分這些網站,請使用它。

網站框架主要基於此模型

class models.Site

用於儲存網站的 domainname 屬性的模型。

domain

與網站相關聯的完整網域名稱。例如,www.example.com

name

網站的易於閱讀的「詳細」名稱。

SITE_ID 設定指定與該特定設定檔案相關聯的 Site 物件的資料庫 ID。 如果省略該設定,get_current_site() 函數將嘗試通過比較 domainrequest.get_host() 方法中的主機名稱來取得目前的網站。

如何使用它取決於您,但 Django 會透過一些慣例自動在幾個方面使用它。

使用範例

為什麼要使用網站?最好透過範例來解釋。

將內容與多個網站關聯

LJWorld.com 和 Lawrence.com 網站由同一新聞機構營運 – 位於堪薩斯州勞倫斯的 Lawrence Journal-World 報紙。LJWorld.com 專注於新聞,而 Lawrence.com 專注於當地娛樂。但有時編輯人員希望在兩個網站上發表文章。

解決此問題的簡單方法是要求網站製作人員將同一個故事發佈兩次:一次用於 LJWorld.com,另一次用於 Lawrence.com。但這對於網站製作人員來說效率低下,並且在資料庫中儲存相同故事的多個副本是多餘的。

更好的解決方案是刪除內容重複:兩個網站都使用相同的文章資料庫,並且文章與一個或多個網站相關聯。在 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 管理介面)中編輯兩個網站上的所有內容。

  • 這表示同一個故事不必在資料庫中發佈兩次;它在資料庫中只有一個記錄。

  • 它讓網站開發人員可以使用相同的 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")
        # ...
    

將內容與單一網站關聯

同樣地,您可以使用 ForeignKey 以多對一的關係將模型與 Site 模型關聯。

例如,如果只允許在單一網站上發佈文章,您將使用類似這樣的模型

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 都具有電子郵件提醒功能,讓讀者可以註冊在發生新聞時收到通知。它非常基本:讀者在網頁表單上註冊,並立即收到一封電子郵件,內容為「感謝您的訂閱」。

實作此註冊處理程式碼兩次是低效且多餘的,因此這些網站會在幕後使用相同的程式碼。但是,每個網站的「感謝您註冊」通知都需要不同。透過使用 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 上,此電子郵件的主旨為「感謝您訂閱 lawrence.com 提醒」。在 LJWorld.com 上,此電子郵件的主旨為「感謝您訂閱 LJWorld.com 提醒」。電子郵件的訊息本文也是如此。

請注意,更靈活(但更繁重)的做法是使用 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 – 包括 https:// 和網域以及所有內容。若要執行此操作,您可以使用網站框架。一個範例

>>> 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 註冊了一個 post_migrate 信號處理器,它會建立一個名為 example.com,網域為 example.com 的預設網站。這個網站也會在 Django 建立測試資料庫後建立。若要為您的專案設定正確的名稱和網域,您可以使用資料遷移

為了在生產環境中提供不同的網站,您可以建立各自的設定檔,每個設定檔都有不同的 SITE_ID(或許可以從一個共用的設定檔導入,以避免重複設定),然後為每個網站指定適當的 DJANGO_SETTINGS_MODULE

快取目前的 Site 物件

由於目前網站儲存在資料庫中,每次呼叫 Site.objects.get_current() 都可能導致資料庫查詢。但 Django 比這更聰明一點:在第一次請求時,目前的網站會被快取,後續的任何呼叫都會返回快取資料,而不是存取資料庫。

如果因任何原因您想強制執行資料庫查詢,可以使用 Site.objects.clear_cache() 告訴 Django 清除快取。

# 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

只有在您的設定中定義了 SITE_ID 設定時,才能使用 CurrentSiteManager

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 物件。

換句話說,這兩個語句是等效的

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

CurrentSiteManager 如何知道 Photo 的哪個欄位是 Site 呢?預設情況下,CurrentSiteManager 會尋找名為 siteForeignKey 或名為 sitesManyToManyField 來進行篩選。如果您使用名稱不是 sitesites 的欄位來識別您的物件與哪些 Site 物件相關,那麼您需要在您的模型上明確地將自訂欄位名稱作為參數傳遞給 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

最後,請注意,您可能需要保留模型上的標準(非特定於網站的)Manager,即使您使用了 CurrentSiteManager。正如在管理器文件中所解釋的,如果您手動定義了一個管理器,那麼 Django 將不會為您建立自動的 objects = models.Manager() 管理器。還要請注意,Django 的某些部分,特別是 Django 管理網站和通用視圖,會使用模型中*首先*定義的管理器,因此如果您希望您的管理網站能夠存取所有物件(而不僅僅是特定於網站的物件),請在定義 CurrentSiteManager 之前,在您的模型中放入 objects = models.Manager()

網站中介軟體

如果您經常使用此模式

from django.contrib.sites.models import Site


def my_view(request):
    site = Site.objects.get_current()
    ...

為了避免重複,請將 django.contrib.sites.middleware.CurrentSiteMiddleware 加入 MIDDLEWARE。該中介軟體會在每個請求物件上設定 site 屬性,因此您可以使用 request.site 來取得目前的網站。

Django 如何使用網站框架

雖然並非必須使用網站框架,但強烈建議您使用,因為 Django 在幾個地方都利用了它。即使您的 Django 安裝僅為單一網站提供服務,您也應該花兩秒鐘建立一個包含您的 domainname 的網站物件,並在您的 SITE_ID 設定中指向其 ID。

以下是 Django 如何使用網站框架

  • 重新導向 框架中,每個重新導向物件都與特定的網站關聯。當 Django 搜尋重新導向時,會將目前的網站納入考量。

  • 靜態頁面 框架中,每個靜態頁面都與特定的網站關聯。當建立靜態頁面時,您會指定其 Site,而 FlatpageFallbackMiddleware 會檢查目前的網站,以檢索要顯示的靜態頁面。

  • 聯合內容 框架中,titledescription 的範本會自動存取變數 {{ site }},該變數是代表目前網站的 Site 物件。此外,如果沒有指定完全限定的網域,則用於提供項目 URL 的鉤子將會使用目前 Site 物件的 domain

  • 驗證 框架中,django.contrib.auth.views.LoginView 會將目前的 Site 名稱以 {{ site_name }} 傳遞給範本。

  • 捷徑視圖(django.contrib.contenttypes.views.shortcut)在計算物件的 URL 時,會使用目前 Site 物件的網域。

  • 在管理框架中,「在網站上檢視」連結會使用目前的 Site 來計算它將重新導向的網站網域。

RequestSite 物件

某些 django.contrib 應用程式會利用網站框架,但其架構方式並不要求網站框架必須安裝在您的資料庫中。(有些人不想要,或只是無法安裝網站框架所需的額外資料庫表格。)對於這些情況,框架提供了一個 django.contrib.sites.requests.RequestSite 類別,當沒有以資料庫為基礎的網站框架時,可以將其作為回退機制。

class requests.RequestSite

一個與 Site 共享主要介面的類別(即,它具有 domainname 屬性),但其資料來自 Django HttpRequest 物件,而不是來自資料庫。

__init__(request)

namedomain 屬性設定為 get_host() 的值。

RequestSite 物件具有與一般 Site 物件相似的介面,但它的 __init__() 方法接受一個 HttpRequest 物件。它能夠透過查看請求的網域來推斷出 domainname。它具有 save()delete() 方法來匹配 Site 的介面,但這些方法會引發 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:80request.get_host() 可能會返回網域和端口。在這種情況下,如果查詢失敗,因為主機與資料庫中的記錄不匹配,則會移除端口,並僅使用網域部分重試查詢。這不適用於 RequestSite,它將始終使用未修改的主機。

返回頂部