“站点”框架

Django 自带了一个可选的“站点”框架。它是一个钩子,用于将对象和功能关联到特定的网站上,它是你的 Django 驱动的网站的域名和“啰嗦”名称的保存地。

如果你的单个 Django 安装支持多个站点,并且你需要以某种方式区分这些站点,那么就使用它。

站点框架主要基于这个模型:

class models.Site

储存网站 domainname 属性的模型。

domain

与网站相关的完全合格域名。例如,www.example.com

name

一个人类可读的“啰嗦”的网站名称。

SITE_ID 配置指定与该特定配置文件相关联的 Site 对象的数据库 ID。如果省略该设置,则 get_current_site() 函数将尝试通过比较 domainrequest.get_host() 方法中的主机名来获取当前站点。

如何使用这个功能由你决定,但 Django 通过几个约定自动使用它。

使用实例

为什么要用站点?通过例子来解释是最好的。

将内容与多个网站关联

LJWorld.comLawrence.com 网站由同一新闻组织——堪萨斯州劳伦斯市的《劳伦斯世界报》经营。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")
        # ...
    

将内容与单一网站关联

同样,你也可以用 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 很脆弱,万一变了呢。比较干净的方法是检查当前网站的域名:

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——包括 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 创建一个单独的配置文件(也许是从一个通用的配置文件中导入,以避免重复的共享设置),然后为每个站点指定适当的 :envar:`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

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 对象。

换个角度看,这两种说法是等价的:

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()
    ...

为了避免重复,在 MIDDLEWARE 中添加 django.contrib.sites.middleware.CurrentSiteMiddleware。中间件会在每个请求对象上设置 site 属性,所以你可以使用 request.site 来获取当前站点。

Django 如何使用站点框架

虽然不要求你使用站点框架,但强烈鼓励你使用,因为 Django 在一些地方利用了它。即使你的 Django 只安装了一个站点,你也应该花两秒钟的时间用你的 domainname 创建站点对象,并在你的 SITE_ID 配置中指向它的 ID。

下面是 Django 如何使用站点框架:

  • 重定向框架 中,每个重定向对象都与一个特定的站点相关联。当 Django 搜索一个重定向时,它会考虑到当前的站点。
  • 简单页面框架 中,每个简单页面都与一个特定的站点相关联。当创建一个简单页面时,你指定它的 SiteFlatpageFallbackMiddleware 在检索要显示的简单页面时检查当前站点。
  • 聚合框架 中,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() 来查找当前站点。

当主机头有一个明确指定的端口时,例如 example.com:80,域名和端口都可能被 request.get_host() 返回。在这种情况下,如果因为主机与数据库中的记录不匹配而导致查找失败,那么端口将被剥离,并且只用域名部分重新进行查找。这不适用于 RequestSite,它将始终使用未修改的主机。