“站点”框架¶
Django 带有一个可选的“站点”框架。它是一个用于将对象和功能与特定网站关联的钩子,也是用于存储 Django 驱动网站的域名和“详细”名称的地方。
如果您单个 Django 安装为多个站点提供支持,并且需要以某种方式区分这些站点,请使用它。
站点框架主要基于此模型
- class models.Site¶
用于存储网站的
domain和name属性的模型。- domain¶
与网站关联的完全限定域名。例如,
www.example.com。
- name¶
网站的可读“详细”名称。
SITE_ID 设置指定与该特定设置文件关联的Site 对象的数据库 ID。如果省略此设置,get_current_site() 函数将尝试通过将domain 与来自request.get_host() 方法的主机名进行比较来获取当前站点。
如何使用它取决于您,但 Django 通过一些约定自动以几种方式使用它。
示例用法¶
为什么要使用站点?最好通过示例来解释。
将内容与多个站点关联¶
LJWorld.com 和 Lawrence.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() 方法。然后,您应该确保您的设置文件中确实包含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 都有电子邮件提醒功能,允许读者注册以在发生新闻时获得通知。它非常基本:读者在 Web 表单上注册并立即收到一封电子邮件,内容为“感谢您的订阅”。
实现此注册处理代码两次效率低下且冗余,因此站点在幕后使用相同的代码。但是“感谢您注册”的通知需要针对每个站点有所不同。通过使用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 上,此电子邮件的主题行为“感谢您订阅 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.txt 和message.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/'
启用站点框架¶
要启用站点框架,请按照以下步骤操作
将
'django.contrib.sites'添加到您的INSTALLED_APPS设置中。定义
SITE_ID设置SITE_ID = 1
运行
migrate。
django.contrib.sites 注册了一个 post_migrate 信号处理器,该处理器创建一个名为 example.com 的默认站点,其域名也是 example.com。Django 创建测试数据库后,也会创建此站点。要为您的项目设置正确的名称和域名,可以使用 数据迁移。
为了在生产环境中服务不同的站点,您可以为每个 SITE_ID 创建一个单独的 settings 文件(也许是从一个公共 settings 文件导入,以避免重复共享设置),然后为每个站点指定合适的 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 关联的对象。
通过显式地将其添加到您的模型中来使用 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 会查找名为 site 的 ForeignKey 或名为 sites 的 ManyToManyField 来进行过滤。如果您使用除 site 或 sites 之外的字段名称来标识您的对象与哪些 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 安装只为单个站点提供支持,您也应该花两秒钟的时间使用您的 domain 和 name 创建站点对象,并在您的 SITE_ID 设置中指向其 ID。
以下是 Django 如何使用站点框架:
在
redirects framework中,每个重定向对象都与特定站点关联。当 Django 搜索重定向时,它会考虑当前站点。在
flatpages framework中,每个静态页面都与特定站点关联。创建静态页面时,您需要指定其Site,并且FlatpageFallbackMiddleware会在检索要显示的静态页面时检查当前站点。在
syndication framework中,title和description的模板会自动访问变量{{ site }},它代表当前站点的Site对象。此外,如果未指定完全限定的域名,则提供项目 URL 的钩子将使用当前Site对象中的domain。在
authentication framework中,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的主要接口(即,它具有domain和name属性),但它从DjangoHttpRequest对象而不是数据库获取数据。- __init__(request)¶
将
name和domain属性设置为get_host()的值。
一个RequestSite对象的接口与普通的Site对象类似,除了它的__init__()方法接受一个HttpRequest对象。它能够通过查看请求的域来推断domain和name。它具有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:80,request.get_host()可能会返回域名和端口。在这种情况下,如果查找失败(因为主机与数据库中的记录不匹配),则会去除端口,并仅使用域名部分重新尝试查找。这并不适用于RequestSite,它将始终使用未修改的主机。