The “sites” framework

Django 原生带有一个可选的“sites”框架。它是一个钩子,用于将对象和功能与特定的站点关联,它同时还是域名和你的Django 站点名称之间的对应关系所保存的地方。

如果你的Django 不只为一个站点提供支持,而且你需要区分这些不同的站点,你就可以使用它。

Sites 框架主要依据一个简单的模型:

class models.``Site

用来存储Web站点的domainname 属性的模型

domain

与Web站点关联的域名。

name

Web 站点的名称。

SITE_ID 设置指定与特定的设置文件关联的Site 对象在数据库中ID。如果省略该设置,get_current_site() 函数将会通过比较domainrequest.get_host() 方法中得到的主机名,来得到当前的Site。

怎样使用取决于你,但是django自动的在几个方面通过一些简单的约定使用它。

示例

为什么要使用Sites 框架?这点通过实例来理解的效果最好

关联内容到多个站点

通过Django开发的站点LJWorld.comLawrence.com 是位于Lawrence, Kansas 的同一家机构Lawrence Journal-World newspaper 运营的。LJWorld.com 关注新闻,而Lawrence.com 关注当地的环境问题。但是有时编辑需要发布同一篇文章到两个站点。

无脑的解决方法是要求站点发布者发布同一内容两次:一次到LJWorld.com,一次到 Lawrence.com。但这是很低效的行为,而且在数据库中必须存储同一内容很多次(多副本存储,浪费资源)。

最好的解决方法很简单:两个站点用相同的文章数据库,一篇文章可以关联一个或者多个站点。用Django 模型的术语,它通过Article 模型的一个多对多字段表示:

  1. from django.db import models
  2. from django.contrib.sites.models import Site
  3. class Article(models.Model):
  4. headline = models.CharField(max_length=200)
  5. # ...
  6. sites = models.ManyToManyField(Site)

这很快很好的完成了几件事:

  • 它使得站点编辑者利用一个接口(Django admin)编辑多站点上的所有内容。

  • 它意味着同一个内容不用往数据库存入两次;在数据库中仅仅只有一条记录。

  • 对于两个站点,开发者可以使用相同的Django 视图代码。显示内容的视图代码需要检查,以确保请求的内容属于当前的站点。就像下面一样:

    1. from django.contrib.sites.shortcuts import get_current_site
    2. def article_detail(request, article_id):
    3. try:
    4. a = Article.objects.get(id=article_id, sites__id=get_current_site(request).id)
    5. except Article.DoesNotExist:
    6. raise Http404("Article does not exist on this site")
    7. # ...

关联内容到单独的站点

类似地,你可以用ForeignKey 关联一个模型到Site 模型实现多对一关系。

例如,一篇文章只允许在一个单独的站点,你应该像这样用模型:

  1. from django.db import models
  2. from django.contrib.sites.models import Site
  3. class Article(models.Model):
  4. headline = models.CharField(max_length=200)
  5. # ...
  6. site = models.ForeignKey(Site)

这个好处和上节描述的好处是相同的。

在视图中获得当前的Site

你可以在Django 视图中使用Sites 框架基于正在调用的视图所在的Site 实现特定的功能。例如:

  1. from django.conf import settings
  2. def my_view(request):
  3. if settings.SITE_ID == 3:
  4. # Do something.
  5. pass
  6. else:
  7. # Do something else.
  8. pass

当然,这样硬编码Site ID 比较丑陋。这种硬编码是你最需要尽快修复的。完成这件事情的更清洁的方法是检查当前站点的域名:

  1. from django.contrib.sites.shortcuts import get_current_site
  2. def my_view(request):
  3. current_site = get_current_site(request)
  4. if current_site.domain == 'foo.com':
  5. # Do something
  6. pass
  7. else:
  8. # Do something else.
  9. pass

它还有一个优点是检查Sites 框架是否安装,如果没有安装将返回一个 RequestSite 实例。

如果你不能访问request 对象,你可以使用Site 模型管理器的get_current() 方法。此时,你需要确保你的设置文件包含SITE_ID 设置。下面的示例与前面的示例等同:

  1. from django.contrib.sites.models import Site
  2. def my_function_without_request():
  3. current_site = Site.objects.get_current()
  4. if current_site.domain == 'foo.com':
  5. # Do something
  6. pass
  7. else:
  8. # Do something else.
  9. pass

显示当前的域名

LJWorld.com 和Lawrence.com 都具有邮件通知功能,它让读者注册以在新闻发生时获得通知。这很简单:读者通过网页表单注册,然后立即收到一封邮件说 “感谢您的订阅”。

将这个注册过程的代码实现两次是低效而冗余的,所以这两个站点在后台使用相同的代码。但是每个Site 的“感谢您的订阅”的通知需要不同。通过使用Site 对象,我们可以抽象这个通知并利用当前Site 的namedomain 的值。

下面是该表单处理视图的一个例子:

  1. from django.contrib.sites.shortcuts import get_current_site
  2. from django.core.mail import send_mail
  3. def register_for_newsletter(request):
  4. # Check form values, etc., and subscribe the user.
  5. # ...
  6. current_site = get_current_site(request)
  7. send_mail('Thanks for subscribing to %s alerts' % current_site.name,
  8. 'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
  9. 'editor@%s' % current_site.domain,
  10. [user.email])
  11. # ...

在Lawrence.com 网站上,这封邮件的标题为“Thanks for subscribing to lawrence.com alerts.”。在LJWorld.com 网站上,这封邮件的标题为“Thanks for subscribing to LJWorld.com alerts.”。邮件体的行为相同。

注意,更加灵活(但是更沉重)的方法是使用Django 的模板系统。假设Lawrence.com 和LJWorld.com 具有不同的模板目录(DIRS),你可以很容易地根据模板系统写出:

  1. from django.core.mail import send_mail
  2. from django.template import loader, Context
  3. def register_for_newsletter(request):
  4. # Check form values, etc., and subscribe the user.
  5. # ...
  6. subject = loader.get_template('alerts/subject.txt').render(Context({}))
  7. message = loader.get_template('alerts/message.txt').render(Context({}))
  8. send_mail(subject, message, 'editor@ljworld.com', [user.email])
  9. # ...

在这种情况下,你必须为LJWorld.com 和Lawrence.com 模板目录都创建subject.txtmessage.txt 模板文件。它更灵活,但是也更复杂。

尽可能地发掘Site 对象的用法以删除不需要的复杂性和冗余是个不错的主意。

获取当前域名的url全路径

Django 的get_absolute_url() 可以很方便地获得对象不带域名的URL,但是某些情况下,你可能想显示完整的URL,带有http://和域名以及其它部分。要实现这点,你可以使用Sites 框架。一个简单的示例:

  1. >>> from django.contrib.sites.models import Site
  2. >>> obj = MyModel.objects.get(id=3)
  3. >>> obj.get_absolute_url()
  4. '/mymodel/objects/3/'
  5. >>> Site.objects.get_current().domain
  6. 'example.com'
  7. >>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
  8. 'http://example.com/mymodel/objects/3/'

启用Sites 框架

按照以下步骤启用Sites 框架:

  1. 添加'django.contrib.sites' 到你的INSTALLED_APPS 设置中。

  2. 定义SITE_ID 设置:

    1. SITE_ID = 1
  3. 运行migrate

django.contrib.sites 注册一个post_migrate 信号处理器,它创建一个默认的Siteexample.com,其域名为example.com。在Django 创建测试数据库之后,也会创建该Site。你可以使用数据迁移来为你的项目设置正确的name 和domain。

为了在线上环境中启用多个Site,你应该为每个SITE_ID 创建一个单独的设置文件(可以从一个共同的设置文件导入,以避免重复共享的配置),然后为每个Site 指定合适的DJANGO_SETTINGS_MODULE

缓存当前Site

因为当前站点储存在数据库,每一次调用 Site.objects.get_current()都会导致数据库查询。但是Django还是比这个聪明滴, 当前站点被放在缓存当中了, 所以后续的调用返回的都是缓存的数据而不是直接查询数据库。

如果出于一些原因你想要强制用数据库查询, 你可以告诉Django清除缓存,用下面这个方法 Site.objects.clear_cache():

  1. # First call; current site fetched from database.
  2. current_site = Site.objects.get_current()
  3. # ...
  4. # Second call; current site fetched from cache.
  5. current_site = Site.objects.get_current()
  6. # ...
  7. # Force a database query for the third call.
  8. Site.objects.clear_cache()
  9. current_site = Site.objects.get_current()

CurrentSiteManager

class managers.``CurrentSiteManager

如果 Site 在你的应用中非常的关键, 你可以考虑用 CurrentSiteManager 在你的模型中(s). 它是一个 modelmanager用来自动过滤,留下只与当前站点有关的数据查询 Site.

必须SITE_ID

CurrentSiteManager 只有在你定义了SITE_ID 在setting 中才起作用。

使用 CurrentSiteManager ,你只要直接把他添加到你的model 中。例如:

  1. from django.db import models
  2. from django.contrib.sites.models import Site
  3. from django.contrib.sites.managers import CurrentSiteManager
  4. class Photo(models.Model):
  5. photo = models.FileField(upload_to='/home/photos')
  6. photographer_name = models.CharField(max_length=100)
  7. pub_date = models.DateField()
  8. site = models.ForeignKey(Site)
  9. objects = models.Manager()
  10. on_site = CurrentSiteManager()

通过这个model, Photo.objects.all() 将会返回所有在数据库中的 Photo对象,但是 Photo.on_site.all()只会返回 与当前site相关的Photo对象, 这是根据 SITE_ID 在setting的设置。

换句话说,这两种表达方式是等价的:

  1. Photo.objects.filter(site=settings.SITE_ID)
  2. Photo.on_site.all()

CurrentSiteManager是如何知道哪个Photo字段是 Site的? 通常来说, CurrentSiteManager查找一个 ForeignKey 它的名字叫site 或者是一个 ManyToManyField字段 ,叫做 sites来筛选出. 如果你用名字不叫site or sites的字段来表示一个与Site对象相关联,,那么你就需要在你的model中显示得传递自定义的字段名给CurrentSiteManager。下面的model, 它有一个字段叫做 publish_on, 说明了这个问题:

  1. from django.db import models
  2. from django.contrib.sites.models import Site
  3. from django.contrib.sites.managers import CurrentSiteManager
  4. class Photo(models.Model):
  5. photo = models.FileField(upload_to='/home/photos')
  6. photographer_name = models.CharField(max_length=100)
  7. pub_date = models.DateField()
  8. publish_on = models.ForeignKey(Site)
  9. objects = models.Manager()
  10. on_site = CurrentSiteManager('publish_on')

如果你尝试使用CurrentSiteManager 并且传递了一个并不存在的字段名称给他, Django 就会引发一个 ValueError.

最后, 注意你可能会想要保持一个正常的 (non-site-specific) Manager 在你的model, 虽然你使用了 CurrentSiteManager. 就像 manager documentation当中的解释那样,如果你手动定义了一个manager,Django是不会为你自动创建 objects = models.Manager() manager。也请注意某些 Django组件 –即, Django admin site 和通用视图– 使用的是 first定义 在你model中的manager,所以如果你希望你的admin site可以连接到所有对象 (不仅仅是特定的站点对象), 那就设置 objects = models.Manager() 在你的 model中, 并且在你定义CurrentSiteManager之前。

网站中间件

New in Django 1.7.

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

  1. from django.contrib.sites.models import Site
  2. def my_view(request):
  3. site = Site.objects.get_current()
  4. ...

这里有些方法可以防止这种重复调用。添加 django.contrib.sites.middleware.CurrentSiteMiddlewareMIDDLEWARE_CLASSES. 中间件设置 site 属性给每一次request对象, 所以你可以用 request.site 来获取当前site。

Django是如何使用的站点框架

虽然不强制要求你的网站使用site框架,但是我们鼓励你使用它,因为在一些地方Django利用它。 即使你的Django只在支持单个站点, 你也应该花两秒时间来给你的站点对象创建domainname,并且设置它的ID在你的 SITE_ID setting中。

下面是Django 如何使用sites framework:

  • redirects framework,每一个redirect都和特定的站点相关联。当Django查找一个 redirect, 它就考虑在当前的站点中查找。
  • flatpages 框架, 每一个flatpage 都被关联到特定的站点。当一个 flatpage 被创建, 你指定它的 Site,并且FlatpageFallbackMiddleware 在返回flatpages 中检查当前站以显示。
  • syndication framework中, 模板的 title and description 自动访问变量{{ site }}, 这个 Site 代表当前站点的站点对象。. 此外,挂钩提供项URL将使用当前 Site对象的domain,如果你不指定一个完全合格的域名。
  • authentication framework中, django.contrib.auth.views.login() 视图传递当前 Site 名称的模板{{ site_name }}.
  • 快捷视图 (django.contrib.contenttypes.views.shortcut) 使用当前Site对象的的域 计算对象的URL。
  • 在管理框架, “view on site” 链接使用当前 Site 算出将重定向的域名.

RequestSite objects

一些 django.contrib应用有利用到 sites framework 但是它们的架构不会require sites framework必须安装在你的数据库中。有些人不想, 或者不能安装site framework所要求的able在他们的数据库中。) 出于这种情况,framework 提供了一个 django.contrib.sites.requests.RequestSite类,当你数据支持的站点框架不可用的时候做一个回退

class requests.``RequestSite

一个共享Site(即,它具有domainname属性)的主接口,但从Django HttpRequest对象,而不是从数据库。

__init__(request)

namedomain属性设置为get_host()的值。

自1.7版起已弃用:此类过去在django.contrib.sites.models中定义。旧的导入位置将工作,直到Django 1.9。

除了其__init__()方法采用HttpRequest对象,RequestSite对象具有与正常Site它可以通过查看请求的域来推断domainname。它具有save()delete()方法来匹配Site的接口,但是方法产生NotImplementedError

get_current_site shortcut

最后,为了避免重复的回退代码,site framework 提供了一个 django.contrib.sites.shortcuts.get_current_site() 功能。

shortcuts.``get_current_site(request)

这是函数是用来检查django.contrib.sites 是否安装并且返回一个基于request的Site 对象或者一个RequestSite 对象。

自1.7版起已弃用:此函数用于在django.contrib.sites.models中定义。旧的导入位置将工作,直到Django 1.9。

Changed in Django 1.8:

如果未定义SITE_ID设置,此函数现在将根据request.get_host()查找当前站点。