Skip to main content

“网站”框架

Django带有一个可选的“sites”框架。它是一个钩子,用于将对象和功能与特定网站相关联,它是域名和“详细”名称的Django供电网站的保持地方。

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

sites框架主要基于一个简单的模型:

class models.Site

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

domain

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

Changed in Django 1.9:

domain 字段设置为 unique

name

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

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

如何使用这个由你自己决定,但Django通过简单的约定以多种方式自动使用它。

用法示例

为什么要使用网站?最好通过例子解释。

将内容与多个网站相关联

Django供电的网站 LJWorld.comLawrence.com 由同一个新闻机构 - 位于堪萨斯州劳伦斯的劳伦斯日报 - 世界报纸。 LJWorld.com专注于新闻,而Lawrence.com专注于本地娱乐。但有时编辑想在 both 网站上发表文章。

解决问题的天真方法是要求网站制作者发布同一个故事两次:一次为LJWorld.com和再次为Lawrence.com。但是对于网站制作者来说效率低下,并且在数据库中存储同一故事的多个副本是多余的。

更好的解决方案很简单:两个网站使用相同的文章数据库,并且文章与一个或多个网站相关联。在Django模型术语中,它由 Article 模型中的 ManyToManyField 表示:

from django.db import models
from django.contrib.sites.models import Site

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.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site, on_delete=models.CASCADE)

这具有与上一节中描述的相同的好处。

从视图挂钩到当前网站

您可以使用Django视图中的sites框架根据调用视图的网站执行特定操作。例如:

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

这还具有检查sites框架是否已安装的优点,并且如果不是,则返回 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 对象,我们可以抽象“谢谢”通知,以使用当前网站的 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, Context

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...

    subject = loader.get_template('alerts/subject.txt').render(Context({}))
    message = loader.get_template('alerts/message.txt').render(Context({}))
    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:// 和域和一切。为此,您可以使用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/'

启用站点框架

要启用网站框架,请按照下列步骤操作:

  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比这更聪明:在第一个请求,当前网站被缓存,任何后续的调用返回缓存的数据,而不是击中数据库。

如果由于任何原因你想强制数据库查询,你可以告诉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 关联的对象。

强制性 SITE_ID

仅当在您的设置中定义 SITE_ID 设置时,CurrentSiteManager 才可用。

通过将 CurrentSiteManager 显式添加到模型中来使用它。例如:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/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.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/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管理网站和通用视图 - 使用在模型中定义 first 的任何管理器,因此如果您希望您的管理网站有权访问所有对象(而不仅仅是网站特定的),在你的模型中放置 objects = models.Manager(),在你定义 CurrentSiteManager 之前。

网站中间件

如果你经常使用这种模式:

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如何使用sites框架

虽然不要求您使用sites框架,但强烈鼓励,因为Django在几个地方利用它。即使您的Django安装仅为一个站点供电,您应该花两秒钟使用您的 domainname 创建站点对象,并指向其在您的 SITE_ID 设置中的ID。

下面是Django如何使用sites框架:

  • redirects framework 中,每个重定向对象与特定站点相关联。当Django搜索重定向时,它会考虑当前网站。

  • flatpages framework 中,每个平面页面与特定站点相关联。创建平面页面时,您可以指定其 Site,并且 FlatpageFallbackMiddleware 会在检索要显示的平铺页面时检查当前站点。

  • syndication framework 中,titledescription 的模板自动访问变量 {{ site }},它是表示当前站点的 Site 对象。此外,如果未指定完全限定域,用于提供项目URL的钩子将使用来自当前 Site 对象的 domain

  • authentication framework 中,django.contrib.auth.views.login() 视图将当前 Site 名称作为 {{ site_name }} 传递给模板。

  • 快捷方式视图(django.contrib.contenttypes.views.shortcut)在计算对象的URL时使用当前 Site 对象的域。

  • 在管理框架中,“在网站上查看”链接使用当前的 Site 来确定将重定向的网站的域。

RequestSite 对象

一些 django.contrib 应用程序利用站点框架,但是以不会在您的数据库中安装站点框架的方式 require 架构。 (有些人不想要,或者只是不是 able 来安装sites框架需要的额外的数据库表。)对于这些情况,框架提供了一个 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() 查找当前站点。

当主机头具有明确指定的端口时,request.get_host() 可以返回域和端口。 example.com:80。在这种情况下,如果查找失败,因为主机与数据库中的记录不匹配,则端口将被删除,并且仅使用域部件重试查找。这不适用于将始终使用未修改主机的 RequestSite

Changed in Django 1.9:

已添加使用剥离的端口重试查找。