“站点”框架 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/ref/contrib/sites
跳转至:导航、​搜索

“站点”框架

Django 带有一个可选的“站点”框架。 它是一个用于将对象和功能与特定网站相关联的钩子,它是您的 Django 站点的域名和“详细”名称的存放处。

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

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

class models.Site

用于存储网站的 domainname 属性的模型。

domain

与网站关联的完全限定域名。 例如,www.example.com

name

网站的人类可读的“详细”名称。

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


将内容与单个站点相关联

同样,您可以使用 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() 方法。 然后,您应该确保您的设置文件确实包含 :setting:`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 有不同的模板目录( :setting:`目录 ` ),您可以像这样使用模板系统:

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' 添加到您的 :setting:`INSTALLED_APPS` 设置。

  2. 定义一个 :setting:`SITE_ID` 设置:

    SITE_ID = 1
  3. 运行 :djadmin:`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。 它是一个模型 manager,它自动过滤其查询以仅包括与当前 Site 关联的对象。

强制 :设置:`SITE_ID`

CurrentSiteManager 仅在您的设置中定义了 :setting:`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() 将仅返回与当前站点关联的 Photo 对象,根据:setting:`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。 如 manager 文档 中所述,如果您手动定义管理器,那么 Django 将不会为您创建自动 objects = models.Manager() 管理器。 还要注意 Django 的某些部分——即 Django 管理站点和通用视图——使用模型中定义的 first 中的任何管理器,所以如果你希望你的管理站点可以访问所有对象(不仅仅是站点特定的),在定义 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 添加到 :setting:`MIDDLEWARE`。 中间件在每个请求对象上设置 site 属性,因此您可以使用 request.site 获取当前站点。


Django 如何使用站点框架

尽管不要求您使用站点框架,但强烈建议您使用它,因为 Django 在一些地方利用了它。 即使您的 Django 安装仅支持单个站点,您也应该花两秒钟时间使用 domainname 创建站点对象,并在 中指向其 ID:设置:`SITE_ID` 设置。

以下是 Django 使用站点框架的方式:

  • 重定向框架 中,每个重定向对象都与特定站点相关联。 当 Django 搜索重定向时,它会考虑当前站点。
  • flatpages 框架 中,每个flatpage 都与一个特定的站点相关联。 创建平面页面时,您指定其 Site,并且 FlatpageFallbackMiddleware 在检索要显示的平面页面时检查当前站点。
  • 联合框架 中,titledescription 的模板自动可以访问变量 模板:Site,即 Site表示当前站点的对象。 此外,如果您未指定完全限定的域,则用于提供项目 URL 的挂钩将使用当前 Site 对象中的 domain
  • 认证框架中,django.contrib.auth.views.LoginView将当前的站点名称作为模板:Site name传递给模板。
  • 快捷视图 (django.contrib.contenttypes.views.shortcut) 在计算对象的 URL 时使用当前 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 对象的函数。 如果未定义 :setting:`SITE_ID` 设置,它会根据 request.get_host() 查找当前站点。

request.get_host() 当 Host 标头明确指定了端口时,域和端口都可以返回,例如 example.com:80。 在这种情况下,如果由于主机与数据库中的记录不匹配而导致查找失败,则会剥离端口并仅使用域部分重试查找。 这不适用于将始终使用未修改主机的 RequestSite