“站点”框架

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 网站由同一新闻机构运营——堪萨斯州劳伦斯的劳伦斯日报新闻报。LJWorld.com 专注于新闻,而 Lawrence.com 专注于当地娱乐。但有时编辑希望在两个网站上发布一篇文章。

解决此问题的简单方法是要求网站制作人员发布同一篇新闻两次:一次用于 LJWorld.com,一次用于 Lawrence.com。但这对网站制作人员来说效率低下,而且在数据库中存储同一新闻的多个副本是冗余的。

更好的解决方案是消除内容重复:两个网站都使用同一文章数据库,并且一篇文章与一个或多个网站相关联。在 Django 模型术语中,这由 ManyToManyFieldArticle 模型中表示

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")
        # ...
    

将内容与单个站点关联

类似地,你可以使用 Site 模型中的多对一关系,将模型关联到 ForeignKey

例如,如果一篇文章只允许在一个站点上,你可以使用类似这样的模型

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 发生更改,硬编码站点 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, "[email protected]", [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 注册一个 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() 将仅返回与当前站点关联的 Photo 对象,根据 SITE_ID 设置。

换句话说,以下两个语句是等效的

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

最后,请注意,即使您使用 CurrentSiteManager,您可能仍希望在模型中保留一个普通(非特定于网站的)Manager。如 管理器文档 中所述,如果您手动定义管理器,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 使用站点框架的方式

  • redirects framework 中,每个重定向对象都与一个特定站点相关联。当 Django 搜索重定向时,它会考虑当前站点。
  • flatpages framework 中,每个扁平页面都与一个特定站点相关联。创建扁平页面时,你指定它的 Site,而 FlatpageFallbackMiddleware 在检索要显示的扁平页面时检查当前站点。
  • syndication 框架 中,titledescription 的模板会自动访问变量 {{ site }},它是表示当前站点的 Site 对象。此外,如果你未指定完全限定域名,用于提供项目 URL 的钩子会使用当前 Site 对象的 domain
  • authentication 框架 中,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 头部明确指定了端口时,request.get_host() 可能会返回域和端口,例如 example.com:80。在这种情况下,如果查找失败,因为主机与数据库中的记录不匹配,则会去掉端口,并仅使用域部分重试查找。这不适用于 RequestSite,它始终会使用未修改的主机。

返回顶部