“站点”框架¶
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 网站是由同一家新闻机构运营的——即堪萨斯州劳伦斯的 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") # ...
将内容与单一网站关联¶
同样,你也可以用 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
It's fragile to hardcode the site IDs like that, in case they change. The cleaner way of accomplishing the same thing is to check the current site's domain:
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 对象,我们可以抽象出“感谢你”通知,使用当前站点的 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:// 和域名等 —— 用于某个对象。为此,你可以使用 sites 框架。示例:
>>> 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 创建一个单独的配置文件(也许是从一个通用的配置文件中导入,以避免重复的共享设置),然后为每个站点指定适当的 :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 相关的对象。
使用 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 会寻找一个名为 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。
最后,请注意,你可能会希望在你的模型上保留一个正常的(非特定站点的)``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 只安装了一个站点,你也应该花两秒钟的时间用你的 domain 和 name 创建站点对象,并在你的 SITE_ID 配置中指向它的 ID。
下面是 Django 如何使用站点框架:
在
重定向框架中,每个重定向对象都与一个特定的站点相关联。当 Django 搜索一个重定向时,它会考虑到当前的站点。在
简单页面框架中,每个简单页面都与一个特定的站点相关联。当创建一个简单页面时,你指定它的Site、FlatpageFallbackMiddleware在检索要显示的简单页面时检查当前站点。在
聚合框架中,title和description的模板会自动访问一个变量{{ 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的主要接口的类(即它有domain和name属性),但它的数据是从 DjangoHttpRequest对象而不是数据库中获取的。- __init__(request)¶
将
name和domain属性配置为get_host()的值。
A RequestSite object has a similar
interface to a normal Site object,
except its __init__()
method takes an HttpRequest object. It's able to deduce
the domain and name by looking at the request's domain. It has
save() and delete() methods to match the interface of
Site, but the methods raise
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,它将始终使用未修改的主机。