Showing posts with label Django 开发指南. Show all posts
Showing posts with label Django 开发指南. Show all posts

Wednesday, March 18, 2009

高级查询和浏览功能

给应用程序提供更多的方式来浏览数据无疑会吸引更多的用户。不同的用户可以通过不同的方式获得他们想要的内容。一些用户喜欢通过浏览分类目录来获得他们想要的信息,而其他用户可能希望搜索特定的主题。有些用户甚至希望只要有新的信息出现就立即收到这些信息。

如今内容推送技术越来越流行,很多用户都在利用这种技术来接收信息。本章我们就学习如何使用Django提供的feed框架给我们的应用程序增加内 容推送功能。我们将为用户提供内容订阅服务,来接收信息。接下来我们还要改进应用程序的查询功能,让查询的结果更准确,这期间还将学到更多Django数 据库API的知识。最后我们还将改进书签列表显示的方式,我们会将列表分多页显示。你将发现本章讲解了很多有意思的技术,在本章你将学到以下内容:

  • 增加RSS订阅功能。
  • 高级搜索功能。
  • 增加列表分页功能。

增加RSS订阅功能

如今很多流行的站点诸如:博客、Wiki 和SNS等等,如果用户使用这些服务就必须时常浏览这些站点以便查看最新的更新内容。如果你必须跟踪很多站点的内容更新,这种方式效率很差。幸运的是一种 称为网络推送服务的技术可以让用户及时的获得最新的更新内容。从概念上讲网络推送技术包含以下几个方面:

  • 一个包含站点最近更新内容的XML文档,这个文档称为 web feed
  • 通过称为feed reader或者aggregater的程序用户可以利用这个文档来订阅最新的更新。
  • 这个程序会将站点最新的更新推送给订阅它的用户。

网络订阅技术成为了一种方便而高效的获取最新更新的技术,因此它迅速在网站和用户之中流行了起来。如今提供内容订阅功能已经称为了Web2.0应用 的标准功能。有很多类型的订阅,比如针对最近更新的订阅、针对最首欢迎内容的订阅和针对某些特定主题的订阅等等。而且内容订阅程序也集成在了许多浏览器和 邮件客户端中。

由此可见,给我们的项目中增加订阅功能有很多好处。比如我们给特定用户提供最新的书签信息或者推送最新的书签评论信息,又或者我们可以给用户提供某 个标签下的书签信息。还可以实现很多功能,并且在Django中增加订阅功能很简单。不管我们准备增加多少或者多少种订阅方法都是一样的。

Django提供了功能强大的框架来创建订阅功能,要创建一个订阅之只需要实现一个Python类就可以了,剩下的工作交给Django就可以了。 在本节你将通过创建两种类型的订阅来学习Django的订阅框架,一个用于订阅某个特定用户的书签,另一用于订阅最近增加到站点的书签。本节结束后你就可 以通过Django的订阅框架创建任何你想要的订阅功能,让我们开始吧。

订阅最近发布的书签

我们创建的第一个订阅将显示最近发布的十个书签列表。我们在前面的章节中已经介绍了如何获得最近发布的十条书签的功能:

Bookmark.objects.order_by('-id')[:10]

上面的方法通过对书签的ID进行逆向排序后取出前面的十条的方式来获得结果列表,我们会看到在订阅功能中实现这个功能只需要几行代码。

我们要作的第一步就是定义一个类并让它继承Feed类,这个Feed类是订阅框架的一部分,它位于 django.contrib.syndication包中。为了更好的组织代码,让我们来创建于一个新的文件来包含这个类,在bookmarks文件夹 下创建一个名为feeds.py文件并加入下面的代码:

from django.contrib.syndication.feeds import Feed
from bookmarks.models import Bookmark
class RecentBookmarks(Feed):
title = 'Django Bookmarks | Recent Bookmarks'
link = '/feeds/recent/'
description = 'Recent bookmarks posted to Django Bookmarks'
def items(self):
return Bookmark.objects.order_by('-id')[:10]

让我们解释这些代码的含义:

  • 首先我们导入了Feed类,这是基类。我们还导入了Bookmark模型类,因为我们要查找最近的十个书签。
  • 然后我们定义一个名为RecentBookmarks类,让它继承Feed类。
  • 我们定义了三个属性:订阅的标题、订阅的链接和订阅的简短描述。
  • 最后我们定义了一个items方法,这个方法返回订阅的内容列表。由于我们准备订阅最近发布的十个书签,所以我们在这里重用了以前的代码。

有几种不同的订阅格式,不过最通常的是RSS,所以Django把这种作为了缺省的格式,一个RSS包含两个部分:

  • 第一个部分通过标题、链接、描述文本以及其他属性定义了订阅本身的信息。这些属性可以通过在类中定义属性字段实现。
  • 还有包含订阅条目的列表,每个订阅条目都包含一个标题、链接、描述和其他几个可能的属性字段,后面我们将逐渐讲解这些内容。

上面的items方法返回一个包含Bookmark对象的列表,那么Django是如何将将Bookmark对象映射到上面提到的订阅条目上 呢?Django利用Python对象的__str__方法来取得订阅条目的标题和描述信息,Django利用get_absolute_url方法来定 义如何显示条目的链接。Django可以让我们轻松的自定义这些缺省行为。

我们首先从定义条目的链接开始,打开bookmarks/models.py文件在Bookmark加入下面黑体字部分的代码:

class Bookmark(models.Model):
title = models.CharField(maxlength=200)
user = models.ForeignKey(User)
link = models.ForeignKey(Link)
def __str__(self):
return '%s, %s' % (self.user.username, self.link.url)
def get_absolute_url(self):
return self.link.url
class Admin:
list_display = ('title', 'link', 'user')
list_filter = ('user', )
ordering = ('title', )
search_fields = ('title', )

这个新增的方法非常简单,它简单的返回了书签的链接。现在我们让Django利用对象的字符串表达式作为订阅条目的标题,回头我们再来自定义它。

创建订阅的最后一个步骤是给它定义URL入口。由于我们这里使用了Django提供的订阅框架来为我们的应用程序增加订阅功能,所以给它定义URL 的步骤也比较特殊。我们通过django.contrib.syndication来映射订阅条目的URL,我们通过一个字典对象把订阅条目信息作为参数 传给这个包。打开urls.py 加入下面高亮部分的代码:

import os.path
from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
from bookmarks.views import *
from bookmarks.feeds import *
site_media = os.path.join(os.path.dirname(__file__), 'site_media')
# Make sure you add the feeds dict before the urlpatterns object.
feeds = {
'recent': RecentBookmarks
}
urlpatterns = patterns('',
# Feeds
(r'^feeds/(?P.)/$', 'django.contrib.syndication.views.feed',*
{'feed_dict': feeds}),
)

上面的代码中我们首先导入了feeds模块,并且创建了一个名为feeds的字典对象。这个字典将每一个订阅类对象映射为一个固定的URL,之后我们我们将所有的订阅链接映射到feeds/ 路径下,我们用包含订阅条目的字典对象作为第三个参数。

这看起来有点复杂,不过一旦我们知道他的工作原来就会发现其实他很间的:

  • 我们把视图的URL映射到feeds/下,并且把包含订阅条目的字典对象作为参数传给这个视图。
  • 当请求一个^feeds/recent/$ 链接时,django.contrib.syndication.views.feed视图就会被请求,这时这个视图会搜索feeds后面的字符串(这里 是recent),然后根据这个字符串从feeds字典中查找对应的订阅条目对象。
  • 然后这个试图会输出一个XML格式的订阅文件给用户。

接下来试试我们新增的订阅功能,运行开发服务器并打开浏览器输入下面的地址http://127.0.0.1:8000/feeds/recent/。结果是什么样取决于你使用的浏览器,如果你使用的是Firefox浏览器你看到的是类似下面的页面:

程序运行的很好,这里的使用了书签对象的字符串描述来显示订阅条目的标题和描述信息,这个信息对于调试来说很有用,但是对于使用的用户并不友好,所以我们要自定义这些信息。

自定义订阅条目字段

我们可以通过模板来自定义订阅条目。有很多种方式自定义订阅条目的方式,你可以自定义条目的标题、给他增加描述信息或者显示条目的作者等等。feed视图会在模板目录下查找一个名为feeds的目录,并在这个目录下查找相应的模板文件,模板文件的命名规则依据下面的格式:

feedname_fieldname.html

feedname是订阅条目的名称,这个名称与前面我们定义的feeds字典对象中保存的订阅条目对象的键的名称相同。在我们的 RecentBookmarks订阅对象中,他的名称是recent。如果你要自定义订阅条目的名称就需要修改fieldname。要修改订阅条目的标 题,首先需要在模板路径下创建一个名为feeds的文件夹,并在这个目录下创建一个名为recent_title.html的文件,在这个文件中加入下面 的代码:

{{ obj.title }}

feed视图将Bookmark对象以obj为名称传给模板,所以这里我们用obj对象来输出标签对象的标题。注意,我们这里没有像通常模板中那样 对标题字段进行字符转义处理,那是因为Django的feed框架已经自动处理了。至于描述信息我们现在还不需要添加,所以我们在 templates/feeds/目录下创建一个空的recent_description.html文件。

这两个文件创建之后,我刷新一下浏览器看看有什么变化:

现在页面看起来好多了,你可以自定义很多更多的信息,比如,你可以在订阅条目中增加书签的用户信息、标签信息等。

本节我们实现了第一个书签。接下来我们将指定创建一个针对某个特定用户的订阅,这里我们将介绍书签的高级功能,这里将使用用户名称作为订阅URL的参数。

创建用户书签订阅

除了查看站点上最新发布的书签之外,用户可能还想查看某个特定用户的书签,比如你可能想关注你某个朋友的书签。因此,创建一个针对用户的订阅非常有用,这个订阅会列出某个用户的书签。

实现这个订阅比前面的要稍显复杂,因为在这里我们要将用户名称作为参数传给订阅URL,视图函数会根据参数显示特定用户的书签信息。

显然,为每个用户写一个单独的书签是不现实的。如果能够通过分析订阅的URL来输出订阅内容就好了,幸运的是Django提供了一种优雅的机制来实现这一功能。

这种机制的工作原理如下:如果请求的URL中包含了feeds字典中没有定义的额外信息,Django就会认为所请求的订阅与特定的对象有关(在我 们的例子中是User对象)。Django通过这个额外的信息(在我们的例子中是用户名称)利用订阅对象的get_object方法来获得这个特定对象。 接下来在显示订阅信息时Django会将这个对象传递给它,订阅就根据这个对象来显示订阅条目。

下面我们就通过一个实际的例子来学习如何创建一个用户对象,打开bookmarks/feeds.py文件并加入下面的代码:

from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User
class UserBookmarks(Feed):
def get_object(self, bits):
if len(bits) != 1:
raise ObjectDoesNotExist
return User.objects.get(username=bits[0])
def title(self, user):
return 'Django Bookmarks | Bookmarks for %s' % user.username
def link(self, user):
return '/feeds/user/%s/' % user.username
def description(self, user):
return 'Recent bookmarks posted by %s' % user.username
def items(self, user):
return user.bookmark_set.order_by('-id')[:10]

让我们分别看看这些方法的含义是什么:

  • get_object:如果订阅URL中包含额外的信息,Django就会调用这个方法。这些额外的信息是以bits参数的方式传递,这个 bits是一个字符串数组,他们之间以/符号分隔。比如我们将一个订阅映射为^feeds/user/$,并且请求的连接是^feeds/user /param1/param2/$,那么bits参数将是['param1','param2']。
  • get_obect:这个方法返回与订阅相关的对象。现在这个订阅需要bits参数中的一个元素(这个元素就是用户名称),并且根据用户名称返回User对象。如果这个bits对象是空的,或者根据这个用户名称找不到一个特定的用户,我们就抛出404对象未找到的异常。
  • title,link,description:在我们前面的订阅对象中这些内容是作为对象的属性存在的,现在他们变成了方法。我们这样作是为了 根据用户的名称显示这些信息。Django可以识别出订阅对象中使用的是属性字段还是方法,如果是方法Django就会将关联对象作为参数传给这个方法。
  • items:这个方法会接收订阅相关的User对象,并根据这个对象来输出书签列表。

完成这个订阅类之后,我们应该将他添加到feeds字典对象中,所以打开urls.py文件并加入下面黑体字部分:

import os.path
from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
from bookmarks.views import *
from bookmarks.feeds import *
site_media = os.path.join(
os.path.dirname(__file__),
'site_media'
)
feeds = {
'recent': RecentBookmarks,
'user': UserBookmarks
}

接下来我们为这个订阅创建模板,在templates/feeds/目录下创建一个user_title.html文件,并加入下面的代码:

{{ obj.title }}

最后我们在这个目录下创建一个空的user_description.html文件。

现在我们新的订阅已经创建好了,打开http://127.0.0.1:8000/feeds/user/your_username/(将your_username替换为实际的用户名称),观察一下有什么变化:

创建管理接口

章节介绍

接下来我们需要一个管理接口来维护用户输入的评论内容。实际上管理接口对于任何需要管理和存储数据的web应用程序来说是一个通用的功能。因此 Django提供了一个功能完备的管理接口模块,Django中的这个模块可以说非常棒,因为它简单易用而且功能强大、而且扩展性很强。

本章你将学到一下内容:

  • 激活管理接口。
  • 使用这个管理接口来维护数据内容。
  • 自定义管理接口。
  • 给用户或者用户组分别权限。

激活管理接口

管理接口在Django中是一个应用程序模块,要使用它必须首先在我们的应用程序中将之激活,步骤和我们前面讲的激活用户身份验证程序的步骤类似。

管理接口应用程序位于django.contrib.admin包中,所以第一步是把这个包的路径加入到INSTALLED_APPS变量中,打开settings.py文件找到INSTALLED_APPS变量修改如下:

INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.comments',
'django_bookmarks.bookmarks',
)

接着执行下面的命令在数据库中创建必要的数据表:

$ python manage.py syncdb

现在为了能够访问这个新增的模块我们需要给它定义一个URL入口。在Django提供的管理接口应用程序中有大量的试图函数,所以如果我们分别给这 些试图函数定义URL的话,那将是一个非常繁琐的工作。因此Django为此提供了一中快捷方式。Django把这个模块中所有的URL映射放在了一个单 独的包中:django.contrib.admin.urls,所以Django提供了一个include()函数,这样我们就可以把所有这些URL映 射“包含”到我们当前的应用程序中,打开urls.py文件加入下面的内容:

urlpatterns = (
# Admin interface
(r'^admin/', include('django.contrib.admin.urls')),
)

这和我们通常定义URL映射的方式略有不同,我们告诉Django从django.contrib.admin.urls这个包中获取URL,而且这些URL都位于 ^admin/路径下。这样我们就可以在自己的应用程序中使用管理接口了。

接下来要作的事情就是告诉Django哪些数据模块可以在管理接口中进行维护。要做到这一点就需要在每一个模型类中定义一个内部的名为Admin的类。打开bookmarks/models.py 文件并给Link模型加入下面黑体字部分的代码:

class Link(models.Model):
url = models.URLField(unique=True)
def __str__(self):
return self.url
class Admin:
pass

我们给Link类加上了一个Admin的内部类就是告诉Django这个Link数据模型可以在管理接口中维护。pass关键字是说这个类什么都不做。随后我们会通过这个类来自定义管理界面的行为,现在现让他空着。

使用相同的方法给Bookmark , Tag 和 SharedBookmark 模型加上一个空的Admin内部类。这样他们就可以在管理接口中维护。由于User对象是有Django提供的所以我们不能自己给他加上Admin类,不 过幸好他默认就是包含这个类的。所以他同样可以在管理接口中访问。

接下来让我们启动开发服务器然后在浏览器中输入下面的网址:http://127.0.0.1:8000/admin/。你会看到一个登录页面。还记得我们在创建项目时同时创建了一个超级用户吧,现在就用那个用户登录:


然后你就会看到一个维护数据模型的页面,上面列出了所有你加上了Admin内部类的模型:

用鼠标点击一个模型的连接,你会得到一个保存在数据库中的这个模型的列表。你可以通过这个页面查看、修改或者增加一个新的对象。下图显示了Link数据模型的内容:

Django会根据模型中的字段属性自动产生一个编辑表单。比如对于Link模型,它包含一个唯一的Url文本字段,你可以通过这个表单来查看或者修改一 个Link对象,另外在你提交的死后,表单还会根据模型的验证逻辑进行校验。所以对于Link对象来说如果你要保存一个无效的URL连接,系统会给出一个 错误提示信息,告诉你应该给出一个合法的连接地址。下图显示了保存非法URL连接是的异常页面:

编辑表单会根据模型字段属性显示不同的组件。对于日期字段会显示一个日历组件,对于外键关联字段会显示一个列表组件,等等。下图显示了一个使用日历组件的用户对象表单:

你还将发现管理接口利用模型中__str__方法的返回值来定义模型的显示内容,这对于我们的开发和维护非常有帮助。

你可以试着增加、删除或者修改一些模型对象来检验一下管理接口的功能。你会发现所有改变都可以在管理接口中立即查看。而且管理接口还会跟踪记录你的操作行为并且你可以根据历史操作会滚你的动作。

本节覆盖了使用Django提供的管理接口的大部分内容,这一功能是使用Django进行开发的益处之一,你可以通过一行代码就获得了一个功能丰富的管理接口。

接下来我们将需要到如何自定义管理接口,另外还将了解iiiiDjango提供的权限管理系统中的特性。

自定义管理接口

Django 提供的管理接口功能非常强大而且扩展性强。你只要花几分钟时间就可以得到一个功能完善的管理接口。尽管Django默认提供的管理借口已经提供了丰富的功 能,不过Django还是提供了许多方法来自定义和增强管理接口。你可以指定那些模块可以出现在管理接口中,以及模型结果列表显示的方式,你还可以自定义 管理接口。现在就让我开始学习这些个功能吧。

自定义列表页面

就像我们前面讲的那样,我们在模型类中定要了一个空的Admin类,这个类可以用于自定义管理接口。

让我们通过一个例子来学习,如下图所示页面中显示了书签的列表:

如果将书签的标题、URL和书签的所有者分不同的列显示不是更好吗?实现这一功能只需要一行代码。我们修改bookmarks/models.py中的bookmark模型,将其中Admin类中的pass关键字替换为下面代码中黑体字的部分:

class Bookmark(models.Model):
title = models.CharField(maxlength=200)
user = models.ForeignKey(User)
link = models.ForeignKey(Link)
def __str__(self):
return '%s, %s' % (self.user.username, self.link.url)
class Admin:
list_display = ('title', 'link','user')

现在刷新一下页面看看有什么变化:

现在表格看起来组织的更好了,我们在Admin类中定义了一个list_display元组,这个元组中的元素包含了显示在列表中的字段名。

还有其他一些属性方法可以在Admin类中使用,每一个都是一个元组,其中可以包含一个或者多个元素:

  • list_filter:如果定义了这个属性那么页面上会显示一个側边栏,根据这个元组中定义的字段属性会显示过滤器链接。
  • ordering:这个元组用于定义显示类别中的排序字段有哪些。存在于元组中字段在页面上会有一个排序标志,可以进行升序或者绛序排列。
  • search_fileds:增加元组中定义的字段为搜索字段。

现在就在我们的书签应用程序中应用我们上面提到的这些功能。打开bookmark/models.py文件编辑Bookmark模型,并加入下面黑体字部分的代码:

class Bookmark(models.Model):
title = models.CharField(maxlength=200)
user = models.ForeignKey(User)
link = models.ForeignKey(Link)
def __str__(self):
return '%s, %s' % (self.user.username, self.link.url)
class Admin:
list_display = ('title', 'link', 'user')
list_filter = ('user', )
ordering = ('title', )
search_fields = ('title', )

新增部分增加了以下功能:

  • list_filter:提供按照用户过滤书签的功能。
  • ordering:为标题字段提供排序功能。
  • search_fields:允许用户按照书签的标题进行搜索。

现在刷新一下页面看看有什么变化:

就像你看到的那样我们只用了很少的代码就改进了页面的功能,接下来我们将学习如何修改管理接口模块的模板来对管理接口提供更好的控制。

重载管理接口模板

有时你可能想好修改一下管理接口的页面外观或者将模型部分还一下位置,幸运的是Django提供的管理接口非常容易扩展,你可以很容易的重载它来实现你想要的。

自定义管理接口模板的过程很简单。首先你把管理接口的模板复制到你的项目的模板目录下,然后扩展模板来实现你想要的功能。管理接口模板的位置取决于Django安装的位置,下列出了一些主流操作系统上Django的安装目录(x.x代表Python的版本号):

* Windows: C:PythonXXLibsite-packagesdjango

* UNIX and Linux: /usr/lib/pythonX.X/site-packages/django

* Mac OS X: /Library/Python/X.X/site-packages/django

如果在默认的安装路径下找不到,就查找一下django-admin.py的位置,可能你会找到多个,但是其中一个是位于Django安装目录下的bin目录下。

找到Django的安装路径后,打开django/contrib/admin/templates/,你就会找到管理接口用到的模板,这里有很多文件但是最重要的是:

  • admin/base_site.html:这是所有管理接口的根模板,所有页面都继承自这个模板。
  • admin/change_list.html:这是用于显示模型对象列表的页面。
  • admin/change_form.html:这个模板用于显示修改或者增加数据模型的页面。
  • admin/delete_confirmation.html:这个模板用于删除对象是显示的确认页面。

让我们来试着修改一下这些模板,假设我们想要修改管理接口模板上的文字"Django administration"。要完成这一功能,我们需要在templates目录下创建一个admin目录,然后把 admin/base_site.html 文件复制到这里,然后打开它把所有的"Django"替换为"Django Bookmarks":

{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{{ title|escape }} |
{% trans 'Django Bookmarks site admin' %}{% endblock %}
{% block branding %}

{% trans 'Django Bookmarks administration' %}

{% endblock %}
{% block nav-global %}{% endblock %}

刷新一下页面看看有什么变化:

修改过程很简单不是吗?你可以自己试着检验一下,比如你可能希望在列表页面和编辑页面上加上一个帮助信息。管理接口的模板利用了Django的模板技术,如果你在其中看到不熟悉的标签,你可以查看Django的文档。

用户,用户组和授权

到目前为止,我们一直使用在manage.py syncdb时创建的超级用户登录系统。在实际情况下还有其他一些受信用户会访问管理接口,本节我们将学习如何授权其他用户登录管理接口,并讲解Django的权限管理系统。

不过在我们开始之前,我们需要强调一下:只有受信用户可以访问管理接口,管理接口功能非常强大所以一定要确定用户是你可以信任的才能授权他访问管理接口。

用户权限

如果除了超级用户你的系统里还没有其他用户,那么你可以通过我们在第四章中建立的用户注册程序注册一个用户。另外你也可以通过在管理接口中点击 User下的Add User来增加一个用户。接下来返回到用户列表页面点击你刚才创建的那个用户名称。你会看到一个表单页面,在这个页面上你可以编辑用户信息比如用户名和用 户的email等。在权限管理部分(Permissions)你可以看到一个Staff status复选框。打开这个选项就可以让这个用户登录管理接口;但是这个用户出了可以登录管理接口之后什么都不能作因为我们并没有授权给他。

为了给用户赋予足够的权限来编辑数据模型,你可以打开Superuser status选项,这样这个用户就有了与管理员一样的权限。但是通常给一个用户赋予管理员的权限并不可取。因此你可以通过Django提供的权限管理系统 更灵活的给用户授权。在Superuser status复选框的下面你可以看到一个授权列表,仔细看看这个列表你会发现每个数据模型有三个可用权限:

  • 在数据模型中增加对象的权限。
  • 在数据模型中修改对象的权限。
  • 从数据模型中删除对象的权限。

Django会为包含Admin内部类的所有数据模型自动创建这样的权限模块。你可以通过箭头来给当前的用户授予权限。比如我们可以给一个用户授予 对link,tag和bookmark授予增、删和改的权限。现在登出系统并用刚才我们授权的那个用户登录,你会发现你只能操控link,tag和 bookmark模型。

编辑用户页面的授权功能部分有一个Active复选框,如果你取消选择这个复选框,那么这个用户就无法再登录这个站点或者管理接口。

用户组权限

如果你有许多个有相同权限的用户,而你准备给每个用户单独授权,这无意是一件恐怖的事情。因此,Django提供了另一个用户管理功能;用户组。简 单的说,用户组是用来对拥有相同权限的用户进行分组。你可以创建一个组并给这个组授权,那么当你将一个用户加入到这个组中,那么这个用户就拥有了这个组所 拥有的权限。

创建用户组对象很简单,点击管理接口主页上的Groups,然后点击Add Group。接下来给这个组起个名字并授予一定权限,最后保存它。

要将一个用户加到某个组下面,你可以选择编辑一个用户,然后在Groups部分选择你要将这个用户加入的组。

在视图中使用权限

尽管到目前为止我们之在管理接口中使用到了权限系统,Django同样允许我们在编写视图函数时使用权限功能。你可以在编写试图函数时利用权限功能,给某个函数设置一个或者一组权限,比如对于某些似有信息只有特殊授权的用户可以访问。本节我们将学习如何实现这些功能。

如果你要检验一个用户是否有某个权限,你可以利用User对象的has_perm方法。这方法接收一个代表用户权限的字符串参数:

app.operation_model

app代表应用程序的名称;operation可以是add,change和delete;model是模块的名称,比如我们要检验一个用户是否有增加连接的功能,我们可以这样:

user.has_perm('bookmarks.add_tag')

也可以通过下面的方法检验一个用户是否可以修改书签:

user.has_perm('bookmarks.change_bookmark')

此外,Django还提供了称为"装饰器"的技术,通过它可以限制某个试图只有特定用户可以访问。这个装饰器名为permission_required,它位于django.contrib.auth.decorators包中。

使用这个装饰器和我们之前使用login_required来限制某些视图只有特定用户可以访问的方式类似。假设我们想限制 bookmark_save_page视图只有拥有 bookmarks.add_bookmark权限的用户才可以访问,我们可以通过下们的代码实现:

from django.contrib.auth.decorators import permission_required
@permission_required('bookmarks.add_bookmark', login_url="/login/")
def bookmark_save_page(request):
# [...]

这个装饰器有两个参数:第一个是访问这个试图需要具备的权限,另一个是一个URL,这个URL用于没有权限的用户访问这个视图时重定向的位置。

那么到底使用has_perm还是permission_required方法取决于你需要控制的权限粒度。如果你打算对整个视图函数的访问进行控 制你可以使用permission_required;如果你准备对视图函数内部逻辑进行权限控制,你可以使用has_perm方法。这两个方法应该足够 应付任何有关权限控制的问题。

总结

尽管本章相对较短,但是我们还是学到了很多东西。这充分证明了使用Django你可以用很少的代码实现许多强大的功能。本章你学习了Django强大的管理接口功能,以及如何自定义管理接口,以及如何利用Django提供的功能丰富的权限管理系统。

下面是对本章的一个简短总结:

  • 在项目中激活管理接口的方法
    • 在settings.py文件中的INSTALLED_APPS变量增加django.contrib.admin应用程序。
    • 运行manage.py syncdb来将管理接口的数据模型同步到数据库中。
    • 在urls.py文件中定义管理接口的入口URL。
  • 你可以在数据模型的Admin类中增加以下字段来定制管理接口中页面显示方式。

  • 你可以通过调用User对象的has_perm方法来判断用户是否拥有某个权限。

  • 你可以通过django.contrib.auth.decorators包下的permission_required装饰器来显示某些视图只有特定用户可以访问。

在接下来的一章中你将学到当今几乎所有的Web2.0程序中都有的几个功能。诸如RSS,查询和浏览流行的内容等等,所以接着学习吧。

Sunday, January 11, 2009

使用Ajax技术增强用户界面

使用Ajax技术

介绍


Ajax对于Web2.0来说可以说是里程碑式的技术,Ajax是由一些列技术组成的可以让开发人员实现可交互式、功能丰富的Web应用程序。这些技术在Ajax出现之前已经存在很多年了,然而使用Ajax技术可以让你实现无属性的页面数据交互。


由于我们的项目是一个Web2.0应用程序,所以增加用户体验变得尤为重要。我们项目的成功取决于用户可以通过它方便的实现发布和共享书签的功能。本章将通过使用Ajax技术使用户界面更优化并提高交换性。


本章你将学到以下内容:




  • Ajax以及在Web应用程序中使用它的好处。



  • 如何在Django中安装Ajax架构。



  • 如何使用开源的jQuery架构。



  • 增加标签搜索功能。



  • 不加载新页面直接修改一个书签。



  • 发布书签时,提供标签的自动补全功能。


Ajax以及使用它的好处


Ajax是Asynchronous JavaScript and XML的缩写,意思是异步JavaScript与XML,包括以下技术:




  • HTML和CSS用于构造页面和样式。



  • JavaScript用于动态的访问和操作页面信息。



  • XMLHttpRequest,这是一个有浏览器提供的对象,通过它可以在不刷新页面的情况下实现客户端与服务器之间的数据交互。



  • 一种用于在客户端和服务器端交换数据的数据格式。XML是一种格式,也可以使用HTML,普通文本或者符合JavaScript标准的JSON格式。


使用Ajax时,如果用户发出一个请求无需刷新页面就可以在后台实现客户端到服务器之间数据交换。这样开发人员可以更好的实现页面交换行为并增加用户体验。


正确使用Ajax技术有如下好处:




  • 更好的用户体验。使用Ajax用户可以在不刷新页面的情况下完成功能工作,就像操作一个桌面应用程序一样。



  • 更好的性能。使用Ajax只有请求数据被发送到服务器端,节省可带宽资源提供了应用程序速度。


有很多应用程序使用了Ajax技术,Google的地图服务和Gmail就是很好的例子。实际上这两个应用是使用Ajax的杰出代表。Gmail与其他邮件服务的一个不同之处是他的用户界面,用户在使用Gmail操作是可以不用等待页面的重新加载直接在当前页面完成操作。这确实提高了Gmail的用户体验,使用Gmail时你会感觉你使用的是一个功能丰富、反映快速的应用程序而不再只是一个web站点。


本章介绍了如何在Django中使用Ajax技术,从而提升我们项目的用户体验。我们将实现当今Web应用程序中三种主要的Ajax应用。不过在这之前我们将先来看看使用Ajax框架而不是原生的JavaScript的好处。



在Django中使用Ajax框架


本节我们将选择并在我们的项目中安装一个Ajax框架。当然这不是在Django中使用Ajax所必须的,但是这将大大提高使用Ajax的便利性。下列了使用Ajax的好处:




  • 在不同的浏览器之间JavaScript的实现是不同的。一些浏览器提供了功能完善的实现,而另一些的实现并不完全或者并不是标准的实现。如果不使用Ajax框架,开发人员就要注意不同浏览器中的这些区别,对于同一个功能可能要根据不同的浏览器单独实现。但是,如果使用Ajax框架这一切都由框架来负责,Ajax框架抽象了对不同浏览器JavaScript实现的访问方法。这样我们就可以专注于功能的实现,而不必再操心不同浏览器和它们的限制。



  • 标准JavaScript提供的函数和类无法满足web应用程序的需求。我们不得不变量很多代码来完成大多数常用的功能。因此,即使你不打算使用Ajax框架,你也会发现你不得不编写一个函数库来封装这些JavaScript从而提供他们的重用性。但是既然这里已经有很多现成的优秀的开源框架我们为什么还要再开发一个呢?


目前市面上有很多Ajax框架,大到提供综合解决方案的框架:这些框架提供服务端到服务端、客户端到服务端的组件;小到轻量级的框架:这些框架只提供了客户端到服务器端JavaScript 库封装。根据目前我们项目的需要,一个客户端到服务端的Ajax框架就够了。另外,这个Ajax框架应该能够方便的集成到Django中,而且这个框架最好是轻量级的快速的框架。有很多优秀的框架都能满足我们的需求,比如Prototype,Yahoo! UI Library 和 jQuery。这些框架我都使用过,而且他们都很不错。但是对于当前的项目我准备使用jQuery,因为它是其中最轻量级的一个。而且jQuery有一个活跃的开发社区和大量的插件。如果你熟悉其他的Ajax框架你可以在本章中继续使用他们。不管你使用哪种Ajax框架服务端的Ajax代码都是一样的。


现在你已经了解了使用Ajax框架的好处,现在我们准备把jQuery安装到Django中。


下载并安装jQuery


使用jQuery的好处之一是它只有一个单独的文件,你可以在http://jquery.com上下载它的最新版本。你有两个选择:




  • 未压缩版本:这是标准版本,我建议你在开发时使用这个版本,你将得到一个.js文件,所有的库函数代码都包含在其中。



  • 压缩版本:你得到的还是一个.js文件,不过代码看起来有点混乱。jQuery的开发人员为了减少文件的尺寸,对未压缩版的文件做了一些改动,比如移除空格、重命名变量等等。当你将应用程序部署到生产环境时这个版本非常有用,因为它提供了和未压缩版本一样的功能,但是文件更小了。


我建议你使用未压缩版本,因为在开发阶段你可能想要查看jQuery中的一段代码到底是如何运行的。不过这两个版本提供的功能是一样的,从一个版本切换到另一个只要把文件替换一下就可以了。


下载了jquery-xxx.js(xxx是版本号)文件后,把它重命名为jquery.js,复制到我们项目的site_media文件夹下,接下来把这个文件包含到我们的base.html模板文件中,这样我们就可以在每个集成它页面中使用jQuery了。打开 templates/base.html 文件加入下面代码中黑体字部分:


<head>
<title>Django Bookmarks
{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style
type="text/css" />
<script type="text/javascript"
src="/site_media/jquery.js"></script>

</head>


你也可以把自己的JavaScript代码放在一个单独的.js文件中然后像这样把它包含到html页面中,或者可以向下面这样直接将代码嵌入到HTML中:


<script type="text/javascript">
// JavaScript code goes here.
</script>


建议使用第一种方法,因为把HTML和JavaScript分开使代码更加清晰。由于我们准备使用自己的.js文件,我们需要一种方法将.js文件连接到模板中而不用每次都更改base.html文件。我们来创建一个模板块,将它置于HTML的head标签之间。我们跟这个模板块取名为external,因为它将链接一个外部的文件到当前模板中,打开 templates/base.html 文件加入下面黑体字部分:


<head>
<title>Django Bookmarks {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css"/>
<script type="text/javascript" src="/site_media/jquery.js">
</script>
{% block external %}{% endblock %}
</head>


现在如果子模板中需要使用JavaScript代码,只要重载这个external就可以了。


在我们开始使用Ajax之前,先来大概的介绍一下jQuery框架。


jQuery JavaScript框架


jQuery是一个JavaScript函数库,它用于与HTML文档交换以及操作HTML文档。使用jQuery你可以大大的节省开发时间并且避免了跨浏览器带来的问题。


通常使用jQuery分以下两步:




  1. 选取一个或一组要操作的HTML元素。



  2. 使用jQuery提供的方法来操作这些HTML元素。


选取HTML元素


jQuery提供了一个非常简单的方法来选取HTML元素,你可以通过将HTML元素标签作为字符串参数传递给 $函数来实现元素的选取。下面举几个例子:




  • 如果你想选择一个页面中所有的(<a>)元素,可以通过调用$("a") 函数来实现。



  • 如果你想选取带有 .title class 属性的 <a> 元素,可以通过调用$("a.title")函数来实现。



  • 如果你想选择一个ID属性为#nav的元素,可以通过调用$("#nav")来实现。



  • 如果你想选择属性ID为#nav 元素下的所有(<li>) 元素,可以通过调用$("#nav li")来实现。


$()函数返回一个jQuery对象,一旦你得到这个对象就可以通过这个对象的方法来与HTML元素进行交互了。


jQuery方法


jQuery提供了大量的方法来操纵HTML文档。通过这些方法,你可以显示或者隐藏页面元素,给元素增加监听方法,修改元素的CSS属性,控制页面结构,更重要的是你可以通过jQuery执行Ajax请求。


在学习jQuery之前,我强烈建议你使用FireFox浏览器并安装它的一个插件FireBug,这对于开发和调试jQuery程序非常有帮助。这个扩展提供了一个非常类似于Python控制台的JavaScript控制台,通过这个插件你可以直接输入JavaScript语句并立即看到结果,你不必再单独的创建一个文件来查看输出结果。我们可以在 http://www.getfirebug.com/上下载安装Firebug插件。


如果由于某些原因你不想使用Firefox,Firebug为其他浏览器提供了一个“lite”版本的扩展,这个扩展是一个JavaScript文件。你可以下载这个文件并把它保存到site_media目录下,然后把这个文件包含到templates/base.html文件中:


<head>
<title>Django Bookmarks {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css"/>
<script type="text/javascript" src="/site_media/firebug.js">
</script>
<script type="text/javascript" src="/site_media/jquery.js">


</script>
{% block external %}{% endblock %}
</head>


现在我们来看看如果使用Firebug插件,运行开发服务器然后打开浏览器并按下F12键,试着操作一下。


隐藏和显示元素


让我们先从简单的开始,hide()表示隐藏元素,show()方法表示显示元素。比如下面的例子中我们通过命令来改变导航菜单的行为:


>>> $("#nav").hide()
>>> $("#nav").show()



通过jQuery我还可以在元素显示或者隐藏的时候个他加上一些特效,试试这些方法 fadeOut(), fadeIn(), slideUp() 或者 slideDown() ,你看到了什么?

这些方法和我们前面将的方法类似,比如你执行下面的命令页面上所有的标签都会消失:


>>> $('.tags').slideUp()


访问CSS属性和HTML元素属性


接下来我们学习如何修改页面元素的CSS属性。jQuery提供了一个方法名为css()来执行CSS操作。如果你调用这个方法并把CSS属性名作为参数传递给它,这个方法会返回CSS参数的值:


>>> $("#nav").css("display")
Result: "block"


如果你给这个方法传递第二个参数,那么这第二个参数会被当作第一个参数的值,在方法执行后会重新设置这个CSS属性的参数值:


>>> $("#nav").css("font-size", "0.8em")
Result: <div id="nav" style="font-size: 0.8em;">


实际上你可以向操作CSS一样操作HTML元素的任何属性。同样jQuery提供了一个名为attr()方法。如果你给它传入的是一个元素属性名,那么这个方法返回的是这个属性的值,如果你传入的是一对属性名称/值,就会重新设置这个属性的值。为了测试这个方法,我们打开书签提交表单,输入下面的内容:


>>> $("input").attr("size", "48")
Results:
<input id="id_url" type="text" size="48" name="url">
<input id="id_title" type="text" size="48" name="title">
<input id="id_tags" type="text" size="48" name="tags">


上面的方法将页面中所有的文本输入框的长度修改为48。


另外,jQuery还提供了一些快捷方法来完成通常的取值和赋值操作,比如val()方法返回一个文本输入框的值,如果这个方法有一个参数值,那么就会用参数值来设置文本输入框的值。还有两个方法addClass() 和removeClass()方法用于给HTML元素增加或删除CSS样式,toggleClass()方法用于触发HTML元素的CSS样式,也就是说使它立即生效。


操作HTML文档


现在你已经熟悉了如何控制页面内的元素属性,现在我们来看看如何在页面中增加或者移除元素。要在一个页面元素之前增加一段HTML代码调用方法before()方法,要在元素后面增加HTML代码调用after()方法。你看jQuery的方法名非常好记,不是吗?


现在让我们来测试一下这个两个方法,打开一个页面,输入下面的内容:


>>> $(".tags").before("<strong>(</strong>")
>>> $(".tags").after("<strong>)</strong>")


你可以通过这种方法在页面的任何位置增加你想要的内容,这些内容可以是HTML标记,也可以是任何文本内容,这些方法为操作HTML页面内容提供了非常灵活的方式。


如果你想在页面上移除内容,可以调用remove()方法,比如:


$("#nav").remove()


这个方法不仅是让元素在页面上消失,而是彻底的从页面上删除了元素。你没办法再次获取被移除的元素,比如:


>>> $("#nav")
Result: []


当然,你只是在当前的页面实例中移除了这个元素,它并没有真正的消失,如果你重新打开这个页面,被移除的元素会重新显示。


遍历文档树


尽管jQuery提供的选取器已经很强大了,你可以通过它找到你要的元素,但是有的时候我们希望从HTML文档的某个特殊部分遍历元素。为此,jQuery提供了一系列的方法来完成这些操作, parent() 方法返回元素的一个上层元素, children()方法返回当前元素的所有子元素。find()方法用于找到当前元素组中具体的某个或者某组元素,比如 $("#nav").find("li") 会找到,菜单组中使用的li元素。如果你想访问元素集合中某个特定的元素可以通过get加索引参数的方式,比如: $("li").get(0) 会得到第一个个元素。


控制事件


接下来我们讨论事件控制器。一个事件控制器就是一个JavaScript函数,它在某些特定事件下会被触发,比如单击一个按钮或者提交表单的时候。jQuery提供了大量的方法来维护时间操作,在我们当前的项目中我们只关心鼠标点击和表单提交两种事件。对于鼠标点击事件我们需要用到click()方法,现在我们在Firebug中输入下面的内容:


>>> $("p").after("<button id=\"test-button\">Click me!</button>")


(注意,这里我们必须使用\ 符号来对引号进行转义)


如果现在你点击这个按钮,什么都不会发生。那么,让我们来给他加上事件控制器:


>>> $("#test-button").click(function () { alert("You clicked me!"); })


现在当你再点击这个按钮的时候,你会看到一个对话框。那么到底click方法是如何运行的呢?,首先:


function () { alert("You clicked me!"); }


这是click方法的参数,它定义了一个函数,但是这个函数没有名称,实际上这种结构在JavaScript中称为匿名函数,他用于当你准备定义一个函数把它作为参数传给另一个函数的时候。我们可以不使用匿名函数:


>>> function handler() { alert("You clicked me!"); }
>>> $("#test-button").click(handler)


上面的两种方式效果是一样的,但是第一种更简洁紧凑,我建议你使用匿名函数你会发现这中方式更加简洁而且可读性更高。


控制表单时间和控制鼠标事件类似,你需要先获得一个表单元素,然后调用submit() 方法,在方法的参数中放入你要处理的事件。在以后的章节中的Ajax中会经常使用这个方法。


发送Ajax请求


在我们结束这一小节之前,先来谈谈Ajax请求。jQuery提供了很多想服务器发送Ajax请求的方法,比如 load()方法会在当前选取的元素中根据URL来加载一个页面。还有一些方法用来发送GET和POST请求,并接收返回结果。在后面讲述Ajax中我们会深入讲解这些方法。


接下来是什么


本节中对jQuery的快速指南已经足够应付我们当前项目中需要开发的Ajax功能,结束本章后你可以利用所学给你自己的项目增加更多的Ajax功能。不过记住这里的介绍只是jQuery的冰山一角,如果你想对jQuery有一个综合的了解我建议你阅读《Learning jQuery》这本书,你可以从http://www.packtpub.com/jQuery得到更多有关这本书的信息。


实现书签在线查询功能


让我们通过给项目增加在线查找功能开始我们的Ajax之旅。在线查找功能很简单:用户输入几个字符然后点击“查找”,通过Ajax将查找的字符串发送到后台,然后在当前页面显示查询结果。当前页面不会重新加载,从而节省了网络带宽并且提升了用户体验。


在我们开始实现这个功能之前,一定要记住Ajax开发的一个重要原则:首先在不使用Ajax的情况下实现这个功能,然后给它加上Ajax。这样你才能确保所有的用户都可以使用你的应用程序,包括那些使用了不支持JavaScript或者Ajax技术浏览器的用户。


实现查询


所以,在我们实现Ajax之前先来实现一个通过标题查找书签的视图函数。首先,我们需要创建一个查询表单,请打开 bookmarks/forms.py加入下面的内容:


class SearchForm(forms.Form):
query = forms.CharField(
label='Enter a keyword to search for',
widget=forms.TextInput(attrs={'size': 32})
)


如你所见这是一个非常简单的表单类,只有一个用于输入查询关键字的文本框。


接下来,我们打开bookmarks/views.py文件创建一个用于查询的视图函数:


def search_page(request):
form = SearchForm()
bookmarks = []
show_results = False
if request.GET.has_key('query'):
show_results = True
query = request.GET['query'].strip()
if query:
form = SearchForm({'query' : query})
bookmarks = \
Bookmark.objects.filter (title__icontains=query)[:10]
variables = RequestContext(request, { 'form': form,
'bookmarks': bookmarks,
'show_results': show_results,
'show_tags': True,
'show_user': True
})
return render_to_response('search.html', variables)


除了几个方法调用之外,这个函数非常容易理解。首先我们初始化了三个变量,form用于保存查询表单数据,bookmarks变量保存了要显示的书签记录查询结果,show_results是一个布尔变量,我们通过这个标记区分两中情况:




  • 没有提交任何查询,这种情况下查询页面不显示查询结果。



  • 提交了一个查询,这种情况下页面将显示查询结果,如果没有匹配查询条件的记录就显示“No bookmarks found”(未找到书签)的消息。


我们需要使用 show_results变量,因为光是bookmarks无法判断用户是否提交了查询,因为不过是用户没有提交查询或者是提交的查询字符串没有匹配记录,bookmarks都是空的。


接下来,我们通过request.GET字典对象的has_key方法来判断用户是否提交了查询字符串:


if request.GET.has_key('query'):
show_results = True
query = request.GET['query'].strip()
if query:
form = SearchForm({'query' : query})
bookmarks = Bookmark.objects.filter(title__icontains=query)[:10]


这里我们使用的GET而不是POST,原因是查询页面并不会创建或者修改数据。通常的规则是,如果仅仅是查询数据,我们使用GET对象,如果是在数据库中创建、修改或者删除数据我们使用POST对象。


如果用户提交了查询字符串,我们设置show_results 为True,并且通过调用strip()方法来确保字符串中不包含空格。如果查询关键字去掉空格后不为空,那么我们就根据它创建一个查询表单对象,然后通过Bookmark.objects的filter方法进行查询,这是我们第一次使用filter方法,你可以把它想象成Django中的查询语句,它根据方法参数的内容来进行查询并返回查询结果集。这个方法的参数必须遵循以下规则:


field__operator


注意这里的field和operator中间是两个下划线:其中field是我们要查询的字段名,operator是我们要执行的希望执行的查询方法。下来是常用的查询方法:




  • exact:字段值必须精确匹配要查询的内容。



  • contains:字段值包含要查询的内容。



  • startswith:字段值以查询内容开头。



  • lt:字段值小于要查询的内容。



  • gt:字段值大于要查询的内容。


注意以上方法对于查询内容是大小写敏感的,同时Django还提供了对应的大小写不敏感的方法: iexact, icontains 和 istartswith。


结合以上解释然我们再来看看刚才的视图函数,我们使用icontains来查找标题包含查询内容的数据集合,然后通过Python的数组分片技术取出前十条记录。最后我们将这些变量传递给 search.html 模板。


现在我们在templates目录下创建 search.html页面并输入下面的内容:


{% extends "base.html" %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
{% block content %}
<form id="search-form" method="get" action=".">
{{ form.as_p }}
<input type="submit" value="search" />
</form>
<div id="search-results">
{% if show_results %}
{% include 'bookmark_list.html' %}
{% endif %}
</div>
{% endblock %}


这个页面和我们前面创建的模板非常相似,我们在其中包含了 bookmark_list.html 部分,我们给查询表单设置了ID,并且给查询结果部分的DIV也设置ID,这样我们就可以在后面使用JavaScript来与他们交互。你看,使用include标记为我们节省了很多时间。我们只需要修改这个一个文件就能改变所有引用它的页面,这是一个对于组织和维护模板来说非常有用的技术。


最后别忘了在urls.py加入对视图函数的URL访问入口:


urlpatterns = patterns('',
# Browsing
(r'^$', main_page),
(r'^user/(\w+)/$', user_page),
(r'^tag/([^\s]+)/$', tag_page),
(r'^tag/$', tag_cloud_page),
(r'^search/$', search_page),
)




现在打开 http://127.0.0.1:8000/search/链接测试一下,我们也可以把这个链接加入templates/base.html 页面的导航菜单中:


<div id="nav">
<a href="/">home</a>
{% if user.is_authenticated %}
<a href="/save/">submit</a>
<a href="/search/">search</a>
<a href="/user/{{ user.username }}/">
{{ user.username }}</a>
<a href="/logout/">logout</a>
{% else %}
<a href="/login/">login</a>
<a href="/register/">register</a>
{% endif %}
</div>


现在我们已经实现了查询功能,我们有了一个新查询页面。我们将在本章的后面给他加入Ajax功能:查询过程将在后台执行,页面不会重新加载,你会发现按照我们当前清晰的代码结构,增加Ajax功能非常简单。


实现在线搜索


实现在线搜索我们需要做两件事:




  • 拦截并控制查询表单的提交事件。这可以通过jQuery的submit()方法实现。



  • 通过Ajax在后台加载搜索结果,并把结果插入到当前页面。这可以通过jQuery的load()方法实现,我们后面会介绍这个方法。


jQuery提供了一个load()方法,它用于从服务器取回一个页面并把页面内容插入到所选的元素中。所以在当前的应用程序中远程页面的URL会被当作参数加载。


首先,我们需要修改一下视图函数,当GET对象中包含一个附加的名为ajax的变量时,视图函数值返回搜索结果页面,而不包含其他部分(比如搜索表单、导航菜单等)。也就是说当GET中包含变量ajax时我们可以只返回bookmark_list.html而不用返回search.html,现在打开 bookmarks/views.py文件修改内容如下:


def search_page(request):
[...]
variables = RequestContext(request, {
'form': form,
'bookmarks': bookmarks,
'show_results': show_results,
'show_tags': True,
'show_user': True
})
if request.GET.has_key('ajax'):
return render_to_response('bookmark_list.html', variables)
else:
return render_to_response('search.html', variables)




接着,我们在site_media 文件夹下创建一个名为 search.js的文件,并把它加入templates/search.html中:


{% extends "base.html" %}
{% block external %}
<script type="text/javascript" src="/site_media/search.js">
</script>
{% endblock %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
[...]


现在真正有趣的部分开始了,让我们创建一个函数来加载搜索结果并把它插入到相应的div中,在site_media/search.js中加入如下代码:


function search_submit() {
var query = $("#id_query").val();
$("#search-results").load(
"/search/?ajax&query=" + encodeURIComponent(query)
);
return false;
}


让我们逐行解释它们的含义:




  • 函数首先通过val()方法获得查询关键字的值。



  • 我们通过load() 函数从search_page视图函数获得查询结果,然后将结果内容插入到 id为#search-results 的div元素中。在这里我们第一次使用了encodeURIComponent函数,它用于给query参数进行URL编码,类似于Django视图函数中的urlencode,这样做是为了确保用户输入的关键字中包含的特殊字符被正确的转义。然后我们把这个转义后的关键字链接到/search/?ajax&query=后面。这个URL会请求search_page视图函数然后将变量ajax和query的值传给它。然后视图函数返回结果页面, load()函数将结果页面插入到ID位 #search-results 的div元素中。



  • 我们在函数的最后返回false是为了告诉浏览器,这个函数执行后不进行任何表单提交操作,否则页面会被重新加载。


最后要提到的一点是,我们应该在什么时候调用这个search_submit函数。一个编写JavaScript的原则是我们不能在页面文档结束加载之前操作页面中的元素。因此这个函数应该在页面刚刚被加载后执行。幸运的是jQuery提供了一个在HTML加载后执行函数的方法,现在让我们来使用这个方法,打开site_media/search.js文件输入下面的内容:


$(document).ready(function () {
$("#search-form").submit(search_submit);
});


$(document)代表当前页面的文档元素。注意document没有用引号包含,它是一个浏览器提供的变量而不是一个字符串。ready()方法用于当所选取的元素加载完毕后执行一个函数。所以这里我们告诉jQuery当页面加载完毕后就执行一个函数。我们将一个匿名函数传递给ready() 方法,这个匿名函数将 search_submit 函数绑定到ID位 #search-form的表单的提交事件上。


就是这些了,我们只用了15行代码就实现了我们的在线搜索功能。现在打开http://127.0.0.1:8000/search/链接测试一下,提交一个查询关键字,你会发现查询结果被自动加载到当前页面,而页面并没有刷新。





本节讲述的内容可以应用于任何需要无刷新加载页面的需求。比如你可以给增加评论的页面提供一个预览功能,用户可以在不刷新页面的情况下直接在当前页面预览他准备提交的评论信息。在下一节我们将给项目增加实时修改书签的功能,用户可以在不刷新页面的情况下直接修改书签。


实时修改书签


我们已经实现了大部分的表情编辑功能。你应该还记得前面章节中 bookmarks/views.py中的bookmark_save_page视图函数,对于相同URL的书签我们只保存一条记录,而不是重复的记录,这得益于Django提供的 get_or_create方法,这大大简化了对于书签内容的编辑操作。下面将实现书签的编辑功能,我们需要实现:




  • 将书签的URL通过GET方式传递一个名为url的变量给bookmark_save_page函数。



  • 修改 bookmark_save_page 函数,让他根据传入的url变量值来修改书签的URL。


在实现上述功能之前让我们先给bookmark_save_page 来个瘦身,我们把存储书签的代码单独抽取为一个 _bookmark_save函数。方法前的下划线告诉Python在导入视图函数模块的时候不要把这个函数导入。这个函数需要一个request(请求)对象和一个有效的表单对象作为参数,这个函数用于保存表单中的书签对象并返回保存后的书签对象。请打开 bookmarks/views.py文件并创建这个函数,如果你愿意可以从 bookmark_save_page 函数中把代码直接剪切出来:


def _bookmark_save(request, form):
# Create or get link.
link, dummy = \
Link.objects.get_or_create(url=form.clean_data['url'])
# Create or get bookmark.
bookmark, created = Bookmark.objects.get_or_create(
user=request.user,
link=link
)
# Update bookmark title.
bookmark.title = form.clean_data['title']
# If the bookmark is being updated, clear old tag list.
if not created:
bookmark.tag_set.clear()
# Create new tag list.
tag_names = form.clean_data['tags'].split()
for tag_name in tag_names:
tag, dummy = Tag.objects.get_or_create(name=tag_name)
bookmark.tag_set.add(tag)
# Save bookmark to database and return it.
bookmark.save()
return bookmark


接下来在 bookmark_save_page函数中将你刚才移除的部分替换为_bookmark_save函数:


@login_required
def bookmark_save_page(request):
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(request, form)
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response('bookmark_save.html', variables)


当前 bookmark_save_page 函数的逻辑应该是这样:


if there is POST data:
Validate and save bookmark.
Redirect to user page.
else:
Create an empty form.
Render page.


为了实现编辑书签的逻辑,我们稍微的修改一下变成这样:


if there is POST data:
Validate and save bookmark.
Redirect to user page.
else if there is a URL in GET data:
Create a form an populate it with the URL's bookmark.

else:
Create an empty form.
Render page.


让我们把上面的伪代码实现为Python代码,打开bookmarks/views.py文件编辑 bookmark_save_page函数:


from django.core.exceptions import ObjectDoesNotExist
@login_required
def bookmark_save_page(request):
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(request, form)
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
elif request.GET.has_key('url'):
url = request.GET['url']
title = ''
tags = ''
try:
link = Link.objects.get(url=url)
bookmark = Bookmark.objects.get(
link=link,
user=request.user
)
title = bookmark.title
tags = ' '.join(
tag.name for tag in bookmark.tag_set.all()
)
except ObjectDoesNotExist:
pass
form = BookmarkSaveForm({
'url': url,
'title': title,
'tags': tags
})
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response('bookmark_save.html', variables)


这段代码首先检查GET中是否包含一个url变量,如果有,就通过这个url查找对应的Link和Bookmark对象,并把这些对象的数据绑定到书签表单中。你也许想问我们为什么把获取Link和Bookmark对象的操作放在一个异常处理结构中。老实说如果通过URL找不到这两个对象的时候抛出一个Http404异常也是可以的,但是我们的代码在这里实现为如果找不到这两个对象就创建一个标题和标签为空的书签表单。


好了,现在我们给每个标签加一个编辑的链接,打开 templates/bookmark_list.html 文件,加入如下代码:


{% if bookmarks %}
<ul class="bookmarks">
{% for bookmark in bookmarks %}
<li>
<a href="{{ bookmark.link.url }}" class="title">
{{ bookmark.titleescape }}</a>
{% if show_edit %}
<a href="/save/?url={{ bookmark.link.urlurlencode }}"
class="edit">[edit]</a>
{% endif %}

<br />
{% if show_tags %}
Tags:
{% if bookmark.tag_set.all %}
<ul class="tags">
{% for tag in bookmark.tag_set.all %}
<li><a href="/tag/{{ tag.nameurlencode }}/">
{{ tag.nameescape }}</a></li>
{% endfor %}
</ul>
{% else %}
None.
{% endif %}
<br />
[...]


注意这里编辑链接的URL格式:/save/?url=。由于我们希望只有登录用户可以修改书签,所以我们在这里使用了 show_edit 标记,当它的值为True时才能编辑。另外,应用程序应该不允许用户编辑其他人的书签。现在打开文件bookmarks/views.py,在user_page函数中加入show_edit变量:


def user_page(request, username):
user = get_object_or_404(User, username=username)
bookmarks = user.bookmark_set.order_by('-id')
variables = RequestContext(request, {
'bookmarks': bookmarks,


'username': username,
'show_tags': True,
'show_edit': username == request.user.username,
})
return render_to_response('user_page.html', variables)


表达式 username == request.user.username 的结果只有用户登录并查看的是自己的标签时这个值才为True,这正是我们想要的功能。


最后,我建议改名一下编辑链接的字体,打开site_media/style.css 文件并加入下面的内容:


ul.bookmarks .edit {
font-size: 70%;
}


现在打开页面测试一下我们新增的功能。


实现实时修改书签功能


现在我们已经实现了书签修改功能,现在开始实现有意思的部分通过Ajax增加实时修改功能。


我们通过以下步骤实现这个功能:



  • 我们拦截修改书签的鼠标单击事件,然后通过Ajax从服务器上加载书签编辑表单。然后我们根据书签表单中的内容更新当前页面上的书签信息。
  • 当用户提交书签修改表单时,我们拦截提交事件通过Ajax将修改后的书签数家发送到服务器。服务器端将书签信息保存并返回HTML格式的书签信息替换原来的书签编辑表单。

我们将使用与在线搜索功能非常相似的方法来实现这个功能。首先我们修改bookmark_save_page函数,这样它就可以响应GET中包含ajax变量的请求。接下来我们编写JavaScript代码来接收页面上编辑表单的信息,这样在用户提交书签信息的时候就可以截获书签数家并发送到服务器上保存。


由于我们准备从bookmark_save_page函数中得到一个包含书签编辑信息的表单给Ajax脚本,所以让我们稍稍修改一下模板。创建一个名为bookmark_save_form.html的模板文件,然后将bookmark_save.html文件中的一些部分放入这个新创建的模板文件中:


<form id="save-form" method="post" action="/save/">



{{ form.as_p }}

<input type="submit" value="save" />


</form>


注意这里我们同时还将表单的action 属性修改为/save/,并且给这个表单设置了ID属性,注意这里给表单定义ID属性是非常有必要的。


接下来我们把这个新增的模板加入到bookmark_save.html中:


{% extends "base.html" %}


{% block title %}Save Bookmark{% endblock %}


{% block head %}Save Bookmark{% endblock %}


{% block content %}


{% include 'bookmark_save_form.html' %}


{% endblock %}


好了,现在我们将表单分类成一个单独的模板文件,现在我们修改一下bookmark_save_page函数来加入处理Ajax请求功能。打开文件bookmarks/views.py 修改文件如下:



def bookmark_save_page(request):


ajax = request.GET.has_key('ajax')

if request.method == 'POST':


form = BookmarkSaveForm(request.POST)

if form.is_valid():



bookmark = _bookmark_save(form)


if ajax:



variables = RequestContext(request, {



'bookmarks': [bookmark],

'show_edit': True,

'show_tags': True

})

return render_to_response('bookmark_list.html', variables)

else:



return HttpResponseRedirect(


'/user/%s/' % request.user.username

)

else:


if ajax:


return HttpResponse('failure')

elif request.GET.has_key('url'):



url = request.GET['url']

title = ''

tags = ''

try:


link = Link.objects.get(url=url)

bookmark = Bookmark.objects.get(link=link, user=request.user)

title = bookmark.title

tags = ' '.join(tag.name for tag in bookmark.tag_set.all())

except:


pass

form = BookmarkSaveForm({


'url': url,


'title': title,

'tags': tags

})

else:


form = BookmarkSaveForm()

variables = RequestContext(request, {


'form': form

})

if ajax:



return render_to_response(



'bookmark_save_form.html',


variables

)

else:


return render_to_response(

'bookmark_save.html',

variables

)

让我们来逐个介绍其中的重点部分:


ajax = request.GET.has_key('ajax')

在方法的开始部分我们首先判断GET中是否有一个ajax变量。我们将判断结果保存在一个名为ajax的变量中。之后我们就可以通过这个变量判断浏览器是否发出了Ajax请求:



if form.is_valid():


bookmark=_bookmark_save(form):

if ajax:


variables=RequestContext(request,


{'bookmarks':[bookmark],

'show_edit':True,

'show_tags':True}

)

return render_to_response('bookmark_list.html',varibales)

else:


return HttpResponseRedirect('/user/%/'%request.user.username)

else:


if ajax:


return HttpResponse('failure')

如果我们接收的是POST请求,那么就要判断提交的表单是否为通过了校验,如果通过了校验我们就保存书签表单的数据。接下来我们校验是否是一个Ajax请求,如果是我们就将保存的书签表单数据显示到bookmark_list.html模板上;如果不是一个Ajax请求我们就将页面重定向到用户页面。另一种情况,如果表单没有通过校验,如果是Ajax请求就返回一个'failure'字符串,页面会根据这个字符串显示一个错误消息框;如果不是一个Ajax请求我们什么都不用做,页面会重新加载错误信息会自动显示在相应的页面上:



if ajax:


return render_to_response('bookmark_save_form.html',variables)

else:


return render_to_response('bookmark_save.html',variables)

在方法的最后部分我们处理非POST请求,如果是Ajax请求就返回bookmark_save_form.html模板,否则就返回bookmark_save.html模板。


现在我们的页面可以向处理普通页面一样处理Ajax请求。下面我们加入JavaScript代码来处理Ajax请求。创建一个bookmark_edit.js的文件并放入site_media文件夹。在我们加入JavaScript代码之前先把这个文件包含到user_page.html模板中,打开user_page.html并修改如下:



{% extends "base.html" %}

{% block external %}

<script type="text/javascript" src="/site_media/bookmark_edit.js">

</script>

{% endblock %}

{% block title %}{{ username }}{% endblock %}

{% block head %}Bookmarks for {{ username }}{% endblock %}

{% block content %}

{% include 'bookmark_list.html' %}

{% endblock %}

我们需要在bookmark_edit.js中增加两个方法:



  • bookmark_edit: 这个方法用于处理点击"edit连接的事件,他从服务器端获取书签编辑表单,然后用表单信息替换当前的书签。
  • bookmark_save: 这个方法处理表单提交事件,将表单数据发送到服务器端,并将页面上的表单替换为保存结果数据。

让我们先从第一个方法开始,打开site_media/bookmark_edit.js文件输入如下代码:



function bookmark_edit() {


var item = $(this).parent();

var url = item.find(".title").attr("href");

item.load("/save/?ajax&url=" + escape(url), null, function () {

$("#save-form").submit(bookmark_save);

});

return false;

}

因为这个方法用于处理用户单击"edit"链接的事件,所以这里的this关键字代表这个'edit'链接对象。把这个对象放入到jQuery的$()中然后调用parent()方法就可以获得这个链接对象的父辈元素,这里就是页面上的<li>元素,也就是书签内容(你可以在Firebug的控制台中自己试试看)。


在获得了书签的<li>元素之后,我们可以得到这个元素的title引用,并据此通过attr方法获得得到书签的URL。接下来我们通过load方法将编辑书签表单的代码嵌入到书签页面的HTML页面上。我们这次调用load()方法除了URL外比以前多了两个额外的参数,这两个额外的参数是:



  • 如果我们发送的是POST请求,这里会增加一个键/值对象,但这里我们用的是GET请求,所以这里我们使用一个null参数。
  • 另一个参数是一个JavaScrpit函数,它会在jQuery加载完URL之后调用。在这个函数里我们给书签表单的提交事件绑定一个bookmark_save函数(我们会在一会儿完成这个函数)。

最后这个方法返回false,它告诉浏览器什么都别做。


接下来我们要做的事把这个bookmark_edit方法绑定到edit链接的单击事件上。这里我们使用了$(document).ready()方法:



$(document).ready(function () {


$("ul.bookmarks .edit").click(bookmark_edit);

});

现在如果你单击edit链接后,你会看到页面上显示了一个修改书签的表单,但是你应该会在Firebug的控制台上看到JavaScript出错消息,因为bookmark_save方法还没有实现,现在让我们来实现这个方法:



function bookmark_save() {


var item = $(this).parent();

var data = {


url: item.find("#id_url").val(),

title: item.find("#id_title").val(),

tags: item.find("#id_tags").val()

};

$.post("/save/?ajax", data, function (result) {


if (result != "failure") {

item.before($("li", result).get(0));

item.remove();

$("ul.bookmarks .edit").click(bookmark_edit);

}

else {


alert("Failed to validate bookmark before saving.");

}

});

return false;

}

这里this代表的是页面中书签编辑的表单对象。这个方法的开始部分获得了编辑表单的父辈对象,这里还是书签的<li>元素。接下来通过ID属性获获得每个表单字段的值,然后通过$.post方法将数据发送到服务器,最后方法返回false以避免浏览器提交页面。


你一定猜到了$.post是一个jQuery方法,他向服务器发送POST请求,这个方法有三个参数:



  • 想服务器发送请求的URL。
  • 一个包含请求数据的 键/值 对象。
  • 一个JavaScript函数,这个函数在POST请求执行之后被调用。

值得一提的是jQuery提供了一个名为$get()的方法,这个方法用于向服务器发送请求,它的参数与$post方法的参数相同。


我们使用$post方法将更新后的数据发送给bookmake_save_page视图函数。我们前面曾经说过这个函数在书签保存成功后的书签HTML页面,否则它就返回一个"failure"字符串。因此,这里我们首先校验服务器端是否返回了"failure"字符串。如果请求成功我们通过before()方法在旧的书签前面插入一个新的,然后使用remove()方法将旧的书签数据从HTML文档中删除出去。如果是另一种情况,请求失败了就弹出一个消息对话框。


现在还剩下几个问题:为什么我们要在原来的书签之前插入一个$("li", result).get(0)元素而不是直接显示它呢?如果你看一下bookmark_save_page函数,你会发现它是使用bookmark_list.html模板来组织书签数据。在这个模板中bookmark_list.html包含书签数据的<li>元素嵌入在<ul>元素中。基本上$("li", result).get(0) 这段代码告诉jQuery获得这一组书签<li>元素中的第一个,而这个正是我们想要的。在这段代码中你可以看到jQuery使用$()函数可以通过设置第二个参数来获得HTML中的一个元素。


bookmark_submit被绑定到bookmark_edit函数中,所以我们无需在$(document).ready()中对其进行处理。


最后,当我们将修改后的书签数据加载到页面时,我们再次调用$("ul.bookmarks.edit").click(bookmark_edit)方法将bookmark_edit函数绑定到新增的edit链接上。如果不这样做的的话当你第二次点击这个链接的时候就会跳转到另一个页面。


完成这些代码后你可以测试一下,打开浏览器跳转到书签页面尝试编辑书签,你会发现修改的书签内容会立即显示在当前页面,页面没有刷新:



现在你已经结束了这一节,而且你应该明白了书签实时修改功能的实现方式,你还可以在其它很多场合下使用这种方式,比如你可以实现实时修改书签注释的功能,你只要在当前页面就可以修改注释而不必刷新页面跳转到另一个URL。


在下一节,我们开始实现第三个Ajax功能,帮助用户在提交书签的时候输入标签。


标签的自动补全


我们在本章要实现的最后一个Ajax增强功能是书签的自动补全功能。书签自动补全功能的概念来源于Google提供的“搜索建议”功能。Google的搜索建议功能是根据用户在搜索框中输入的文字,在搜索框下给出最接近搜索关键字的结果列表。很多代码开发集成环境中也提供了自动补全功能,会根据你键入的代码自动给出一列最接近你键入的代码的建议性列表。这一功能节省了用户的输入时间,用户只要输入几个字符就可以根据自动补全的结果列表选择想要输入的内容而不必输入整个标签。


我们也将在本章为标签实现自动补全功能,当用户编辑书签时输入标签系统给自动给出一个建议列表,我们不打算从头实现这个功能,我们准备使用jQuery的插件来实现这个功能。jQuery提供了一些列功能强大的插件。安装jQuery插件与安装jQuery没什么区别。你需要下载一个(或者多个)文件并把它们链接到你的模板文件中,然后通过JavaScript代码来调用这些插件。你可以在http://docs.jquery.com/Plugins上浏览jQuery的插件,然后查找auto-complete插件并下载它,或者直接从下面的链接下载这个插件:


http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/


我们将得到一个包含很多文件的压缩文件,将压缩文件内的文件解压缩到site_media目录下:



  • jquery.autocomplete.css
  • dimensions.js
  • jquery.bgiframe.min.js
  • jquery.autocomplete.js

为了在书签页面中实现自动补全功能,在site_media目录下创建一个空的tag_autocomplete.js文件。然后打开文件templates/bookmark_save.html并把刚才的js文件链接到其中:



{% extends "base.html" %}

{% block external %}

<link rel="stylesheet"

href="/site_media/jquery.autocomplete.css" type="text/css" />

<script type="text/javascript"

src="/site_media/dimensions.js"> </script>

<script type="text/javascript"

src="/site_media/jquery.bgiframe.min.js"> </script>

<script type="text/javascript"

src="/site_media/jquery.autocomplete.js"> </script>

<script type="text/javascript"

src="/site_media/tag_autocomplete.js"> </script>

{% endblock %}

{% block title %}Save Bookmark{% endblock %}

{% block head %}Save Bookmark{% endblock %}

[...]

现在我们完成了插件的安装,如果你读一下插件的文档你会发现可以通过在文本输入框上调用autocomplete()方法来激活这个插件。autocomplete()方法有以下参数:



  • 一个服务器端的URL。插件会将键入的字符以GET的方式发送到这个URL中,并且期待服务器端返回一个搜索结果建议列表。
  • 一个可以用于指定不同可选项的对象。其中之一是multiple变量,这是一个布尔变量,它告诉插件文本输入框可以输入多个值(记住,这里我们使用一个文本框输入多个标签),还有一个是multipleSeparator变量,它告诉插件输入文本框的内容什么用什么字符串进行分隔。在我们的应用中这个分割字符串是空格。

在激活插件之前,我们需要定义一个函数来接收用户的输入并返回建议结果,打开文件bookmarks/views.py并输入下面的内容:



def ajax_tag_autocomplete(request):


if request.GET.has_key('q'):


tags =


Tag.objects.filter(name__istartswith=request.GET['q'])[:10]

return HttpResponse('n'.join(tag.name for tag in tags))

return HttpResponse()

自动补全插件将用户输入的内容保存到GET变量'q'中并发送给服务器。因此我们首先校验是否有这样一个变量,然后根据变量的内容在标签中查找是否有意以此开头的书签,并将查找结果放入一个列表中。我们在这里使用了前面学过的filter和istartswith方法。这里我们只取了前十个搜索结果以避免过多的搜索建议结果把用户搞糊涂了,而且节省了宽带和性能开销。最后我们将搜索结果放在一个字符串中并用换行符分隔。然后将这个字符串放入HttpResponse中返回。


完成了视图函数后,需要在urls.py中给这个视图函数加一个URL入口:



urlpatterns = patterns('',



# Ajax

(r'^ajax/tag/autocomplete/$', ajax_tag_autocomplete),

)

现在我们在site_media/tag_autocomplete.js文件中加入如下代码以激活自动补全插件:



$(document).ready(function () {



$("#id_tags").autocomplete(



'/ajax/tag/autocomplete/',

{multiple: true, multipleSeparator: ' '}

);

});

以上代码给$(document).ready()传入一个匿名函数,这个函数在书签文本框上调用autocomplete()方法,并传入我们刚才讲过的参数。


只要这几行简单的代码就可以实现书签的自动补全功能。我们打开http://127.0.0.1:8000/save/来测试一下,在标签文本框中输入一两个字符,系统会从数据库中查询出建议结果并显示在输入文本框下面:




完成这个功能之后我们也该结束本章的内容了,我们在本章介绍了很多新的技术。读完本章之后你应该考虑在用户页面上实现很多其他增强功能,比如在用户页面上删除书签或者根据标签浏览书签等等。


下一章我们将介绍另一个主题,我们将允许用户都他们感兴趣的书签进行投票或者加上注释,我们将给页面加上更丰富的内容。



总结


喔,这真是很长的一章,不过我们也确实从中学到了很多东西。本章开始的部分我们学习了jQuery框架以及如何将之集成到Django框架中;之后我们在书签应用程序中实现了三个Ajax功能:在线实时搜索、在线实时编辑和书签自动补全功能。


下一章我们准备开始另一个主题,我们将允许用户在页面前端提交书签并对它们感兴趣的书签进行投票,我们还将允许用户给书签加上评论。所以跟着我继续学习吧。






Monday, December 29, 2008

Django 调试工具

当你运行Django程序的时候你是否想知道:
  1. 当前页面使用了哪些模板?
  2. 当前页面是否执行了SQL,如果是执行的SQL是什么(Django把SQL封装了,但是我还是想看看SQL到底是怎么执行的)?
  3. 当前的会话里有些什么?
  4. 日志...
等等这些有用的调试信息。如果是,那么下面我要给你介绍的工具就是你想要的。Django debug tools bar
下面看看如何安装并使用它。
  1. 首先,我们需要下载最新版本的DjangoDebugToolsBar。下载的是一个压缩文件,把它解压缩到你磁盘上的一个位置。
  2. 然后我们准备安装。安装的方式有两种:如果你有python setuptools 那么可以直接运行其中的setup.py。否则,你可以直接将其中的debug_toolbar文件夹及其下面所有的内容加入到你的Python路径中。一个简单的方法是把它直接复制到C:\Python26\Lib\site-packages\  下面。
  3. 然后打开你的Django项目,找到settings.py文件加入一个新的middleware:           'debug_toolbar.middleware.DebugToolbarMiddleware'      注意这里的middleware在MIDDLEWARE_CLASSES中的位置是非常重要的,你应该把它放在所有其它的middleware下面。
  1. 接下来要做的是加入一个INTERNAL_IPS = ('127.0.0.1',) 变量,他定义了你在调试Django是使用的IP是什么。
  2. 把debug_toolbar加入到INSTALLED_APPS中
  3. 在TEMPLATE_DIRS中加入'path/to/debug_toolbar/templates'
  4. 最后新增一个DEBUG_TOOLBAR_PANELS元组,他用于指定你在Django Debug ToolsBar上显示的内容,下面是一个例子:
     DEBUG_TOOLBAR_PANELS = (
   'debug_toolbar.panels.version.VersionDebugPanel',
   'debug_toolbar.panels.timer.TimerDebugPanel',
   'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
   'debug_toolbar.panels.headers.HeaderDebugPanel',
   'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
   'debug_toolbar.panels.template.TemplateDebugPanel',
   'debug_toolbar.panels.sql.SQLDebugPanel',
   'debug_toolbar.panels.cache.CacheDebugPanel',
   'debug_toolbar.panels.logger.LoggingPanel',
)
你在这里也可以调整他们的显示顺序或者编写一个用于你自己项目的调试组件。下面是我装了这个工具后的页面截图:


Sunday, December 21, 2008

标签介绍


给我们的应用程序增加更多的功能

介绍


标签是Web2.0应用程序中的一个重要功能。标签是与一段信息关联的关键字,这个信息可能是一篇文章,一个图片或者一个链接。我们这里说的标签功能就是将一个标签与一个具体的内容建立关联的过程,这一过程通过由标签的创建人或者使用者参与,允许他们对内容进行分类并定义每个分类的标签。标签功能在Web应用程序中非常受欢迎,因为它可以让用户方便的对不同内容进行分类、查看和共享。如果你对标签功能不是很熟悉,你可以看看一些例子,比如del.icio.us 上提供的标签共享功能,在这里针对每个书签会有一系列标签;由或者看看Wikipedia ,这里的标签位于每篇文章的顶部。

由于我们的应用程序是一个社区书签共享程序,所有标签功能对于书签的浏览和共享来说非常重要。为了给我们的应用程序增加标签功能,需要提供一种机制使用户在提交书签的同时将其相关的标签也保持到数据库中,并且要提供一个方法让用户可以浏览属于某个标签分类下的书签。

本章你将学到以下内容:

  • 设计一个标签数据模型。

  • 创建一个书签表单。

  • 创建一个显示某个标签下所有书签的页面。

  • 创建一个标签

  • 限制用户对某些页面的访问。

  • 防止用户输入非法数据。


标签数据模型


我们需要将标签信息保存到数据库中并将之与书签数据关联。所以第一步要做的事情是创建这个标签的数据模型。这个标签对象只有一个代表标签名的字符串信息。另外,我们还需要维护一系列标签与某个特定的书签之间的关联关系。你一定还记得我们在第3章中介绍的通过一个外键将书签与用户之间建立关联的方式,我们称之为一对多关联。然而,标签与书签之间的关系不是一对多的关系,因为一个标签可以和多个书签关联,一个书签可以有多个标签,我们称这种关系为多对多关系,在Django中我们通过models.ManyToManyField来表示这种多对多关联。

现在要做的事情是打开 bookmarks/models.py文件并在其中增加Tag类:

class Tag(models.Model):
  name = models.CharField(maxlength=64, unique=True)
  bookmarks = models.ManyToManyField(Bookmark)

代码非常简洁,我们为标签定义了一个数据模型,这个数据模型中包含了标签的名称和标签对应的书签。创建完数据模型之后我们将新建的模型同步到数据库中,执行下面的命令:

$ python manage.py syncdb

熟悉SQL的开发者都知道,要实现这种多对多关系一般需要建立一个中间表来保存两个表之间的多对多关系,然我们看看Django是如何实现多对多关系的,打开命令行输入执行下面的命令:

python manage.py sql bookmark

下面是输出的结果,新增的部分用黑体字标明:

BEGIN;
CREATE TABLE "bookmarks_link" (
    "id" integer NOT NULL PRIMARY KEY,
    "url" varchar(200) NOT NULL UNIQUE
)
;
CREATE TABLE "bookmarks_bookmark" (
    "id" integer NOT NULL PRIMARY KEY,
    "title" varchar(200) NOT NULL,
    "user_id" integer NOT NULL REFERENCES "auth_user" ("id"),
    "link_id" integer NOT NULL REFERENCES "bookmarks_link" ("id")
)
;
CREATE TABLE "bookmarks_tag" (
    "id" integer NOT NULL PRIMARY KEY,
    "name" varchar(64) NOT NULL UNIQUE
)
;
CREATE TABLE "bookmarks_tag_boomarks" (
    "id" integer NOT NULL PRIMARY KEY,
    "tag_id" integer NOT NULL REFERENCES "bookmarks_tag" ("id"),
    "bookmark_id" integer NOT NULL REFERENCES "bookmarks_bookmark" ("id"),
    UNIQUE ("tag_id", "bookmark_id")
)
;

COMMIT;

不同数据库输出的结果可能略有不同,但是很显然Django也创建了一个中间表名为bookmarks_tag_bookmarks用于维护标签和书签表之间的多对多关系。

值得注意的是当我们在Django中定义多对多关系的时候, models.ManyToManyField 字段可以任意两个相关模型其中之一,也就是说我们也可以把这个字段放在bookmark模型中,这里放在Tag类中只是因为它是在之后创建的。现在让我们打开控制台,看看Django中多对多关系是怎么工作的:

>>> from bookmarks.models import *
>>> bookmark=Bookmark.object.get(id=1)

>>> bookmark=Bookmark.objects.get(id=1)
>>> bookmark.link.url
u'http://www.realasshole.com'
>>> tag1=Tag(name='book')
>>> tag1.save()
>>> bookmark.tag_set.add(tag1)
>>> tag2=Tag(name='publisher')
>>> tag2.save()
>>> bookmark.tag_set.add(tag2)
>>> bookmark.tag_set.all()
[<Tag: Tag object>, <Tag: Tag object>]

现在我们创建了两个标签对象,并将它与一个书签对象关联,尽管我们没有修改Bookmark的数据模型,但是在增加Tag模型之后Django自动为我们增加了一个 tag_set属性,这是因为我们在Tag和Bookmark之间建立了多对多关系。通过这个tag_set属性我们可以访问并控制某个书签下的标签信息。那么如何访问标签对于的所有书签对象呢,答案是Tag对象有一个bookmarks属性,通过它你可以得到一个Tag对象的所有Bookmark对象:

>>> tag1.bookmarks.all()
                  [<Bookmark: Bookmark object>]

现在我们实现了给一个书签增加标签并访问这些书签的功能,在结束本节之前我们还要考虑一个细节问题,当我们打印一个模型数据的时候,我们得到的是一个对象类型字符串,但是我们不知道这个对象到底包含的内容是什么,如果能给这些输出信息加上更多描述,那么可读性就更强了。Django使用Pythong内置的功能实现了这一特性,我们只要在数据模型中增加一个__str__方法,当你打印数据模型对象时,Django 就会用这个方法来显示对象的名称。

让我们打开 bookmarks/models.py 文件并输入下面的内容:

class Tag(models.Model):
  name = models.CharField(maxlength=64, unique=True)
  bookmarks = models.ManyToManyField(Bookmark)
  def __str__(self):
    return self.name

为了检验我们刚才的新增的功能,打开命令行输入下面的内容:

>>> from bookmarks.models import *
>>> Tag.objects.all()
                  [<Tag: book>, <Tag: publisher>]

这些描述性信息多我们的开发和调试都非常有帮助,让我们给Link和Bookmark对象也加上以上的描述信息:

class Link(models.Model):
  url = models.URLField(unique=True)
  def __str__(self):
    return self.url
class Bookmark(models.Model):
  title = models.CharField(maxlength=200)
  user = models.ForeignKey(User)
  link = models.ForeignKey(Link)
  def __str__(self):
    return '%s, %s' % (self.user.username, self.link.url)

全都对象模型都加上了注释User对象由Django为我们提供,它已经定义了__str__方法。现在我们已经为标签定义了数据模型,接下来我们创建一个表单让用户可以提交他们的书签信息。

注意:本文中大部分地方使用了from  X import * 这种方式,这不是Python规范推荐的方式,因为这很容易导致导入了很多不必要的模块。我们在这里这样使用是因为当前的程序中一个模块下包含的内容很少,如果是在一个大型项目中你应该只导入自己需要的部分

创建书签表单

现在我们已经开发了一个将书签和标签存储到数据库的功能,我们将创建一个表单让用户将书签信息保存到数据库中;这表单将允许用户输入书签的标题、URL以及书签的标签。创建书签表单的方式和我们前面讲过的创建用户注册表单的方式相似。实际上这里介绍的创建表单的方法可以用于创建任何将数据保存到数据库的HTML表单。

创建表单的第一步是创建表单类,请打开forms.py文件并输入下面的内容:

class BookmarkSaveForm(forms.Form):
    url=forms.URLField(
                       label="URL",
                       widget=forms.TextInput(attrs={'size':64})
                       )
    title=forms.CharField(
                          label="Title",
                          widget=forms.TextInput(attrs={'size':64})
                          )
    tags=forms.CharField(
                         label="Tag",
                         required=False,
                         widget=forms.TextInput(attrs={'size':64})
                         )
      

上面的代码看起来应该很熟悉,我们为每个字段设置他的文本标签以及HTML窗口部件的行为。在这里我们改变了HTML文本输入框的缺省值,我们通过给一个窗口部件传入字典参数的方式来设置HTML部件的属性,在这里我们将文本输入框的长度设置为64。

在定义了字段的窗口部件行为后,我们不必单独制定这个窗口部件的验证逻辑,因为字段根据本身的类型,Django会自动进行验证,比如对于URLField,Django会校验输入的内容是否是一个合法的URL。

值得注意的是标签字段在这里使用的是HTML文本输入框。用户可以将书签的标签内容用空格分开保存,许多Web应用程序都采用这种方式保存书签。如果在加上Ajax功能使用起来就更方便了,我们在后面的章节中将介绍Ajax的知识。

接下来要做的事情就是给这个表单定义一个视图,和前面定义用户注册表单的视图一样,我们打开bookmarks/views.py文件并定义一个名为bookmark_save_page的视图函数:

from bookmarks.models import *
def bookmark_save_page(request):
  if request.method == 'POST':
    form = BookmarkSaveForm(request.POST)
    if form.is_valid():
      # Create or get link.

      link, dummy = Link.objects.get_or_create(
        url=form.clean_data['url']
      )
      # Create or get bookmark.
      bookmark, created = Bookmark.objects.get_or_create(
        user=request.user,
        link=link
      )
      # Update bookmark title.
      bookmark.title = form.clean_data['title']
      # If the bookmark is being updated, clear old tag list.
      If not created:
        bookmark.tag_set.clear()
      # Create new tag list.
      tag_names = form.clean_data['tags'].split()
      for tag_name in tag_names:
        tag, dummy = Tag.objects.get_or_create(name=tag_name)
        bookmark.tag_set.add(tag)
      # Save bookmark to database.
      bookmark.save()
      return HttpResponseRedirect(
        '/user/%s/' % request.user.username
      )
  else:
    form = BookmarkSaveForm()
  variables = RequestContext(request, {
    'form': form
  })
  return render_to_response('bookmark_save.html', variables)

注意如果你使用的是Django1.0请将clean_data改成cleaned_data。

这个视图函数和用户注册视图的结构相同,只不过是使用了不同的表单对象和模板,以及一个新的将数据存储到数据库中的方法。让我们看看每行代码的含义:

  • 我们首先判断用户请求是Get还是Post,如果是Get就创建一个BookmarkSaveForm并将它传递给对应的模板。

  • 如果是POST请求就将浏览器发过来的数据填充到BookmarkSaveForm对象中,并进行校验。

  • 如果校验通过,我们就创建一个Bookmark对象并把数据存储到数据库中。每个书签都包含:书签的用户、标题、链接以及一组标签。让我们看看怎么得到这些数据。

  • User对象已经保存在 request.user中。

  • 要获得Link对象,我使用了方法Link.objects.get_or_create。这是我们第一次使用这个方法,但是你会发现在使用Django表单对象的手这是一个非常有用的方法。这个方法的用处就像方法名暗示的那样,首先它会根据方法的参数尝试从数据库中找到对应的对象,如果找不到就在数据库中创建一个新的对象。这个方法返回它找到的或者新差创建的对象和一个布尔值,这个布尔值代表是否需要创建新的对象,由于这个布尔信息对我们没什么用处,我们暂时把它存储在一个临时变量中dummy。

  • 书签的标题信息可以通过form.clean_data获取(Django1.0用户请使用cleaned_data)。

  • 现在我们差不多得到了创建一个书签的所有必要信息,我们这里使用get_or_create方法的目的是,我们不希望创建重复的标签,如果一个标签已经存在了,我们就直接拿来用,如果没有我们才创建一个新的,这就是get_or_create方法的妙用。

  • 我们通过直接赋值的方式修改bookmark.title信息。

  • 然后我们通过created的值来判断这个书签是不是已经创建了,如果是一个已经存在的书签,我们需要先把这个书签的标签都删除掉,然后才能增加新的书签。

  • 最后,我们将得到的按空格分开的标签信息转换成一个标签数组,同样适用get_or_create方法来将它们存储到数据库中。

  • 程序结尾的部分是最终表单被发送到一个模板页面bookmark_save.html中,或者如果保存书签成功就返回到用户页面。

若你所见,这段代码很长,但是如果把它们分开了解释每一部分都非常容易理解。接下来要做的事情就是给这个视图定义一个模板和URL入口。标签视图的模板和用户注册视图的模板类似,我们要做的事情就是在页面中展示这个模板的内容。在templates文件夹下创建一个bookmark_save.html 文件,并加入下面的代码:

{% extends "base.html" %}
{% block title %}Save Bookmark{% endblock %}
{% block head %}Save Bookmark{% endblock %}
{% block content %}
<form method="post" action=".">
  {{ form.as_p }}
  <input type="submit" value="save" />
</form>
{% endblock %}

然后是打开url.py  文件输入下面的代码:

(r'^save/$', bookmark_save_page),

你可能已经注意到了,我们的url.py文件内容变得原来越多,是时候整理一下他们了,加些注释和空格,这样更清晰一些:

urlpatterns = patterns('',
  # Browsing
  (r'^$', main_page),
  (r'^user/(\w+)/$', user_page),
  # Session management
  (r'^login/$', 'django.contrib.auth.views.login'),
  (r'^logout/$', logout_page),
  (r'^register/$', register_page),
  (r'^register/success/$', direct_to_template,
     { 'template': 'registration/register_success.html' }),
  (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
     { 'document_root': site_media }),
  # Account management
  (r'^save/$', bookmark_save_page),
)

好了现在一切都准备好了,启动开发服务器并输入下面的地址http://127.0.0.1:8000/save/,你应该会看到类似下面的页面:

现在试着输入一些表单内容,看是否能够保持你的书签信息。你也可以试着输入一个非法的URL,你会发现Django给出的错误提示信息。注意Django中对URL的校验规则是必须要有协议头信息,比如http://www.example.com是合法的URL,但是www.example.com就不是了。

我们在前面的章节中创建了用户页面,那是后还没有标签的功能,所以这个页面上也不会显示标签信息,我们将在后面的小节中介绍如何在也个页面中查看某个标签下的所有书签。不过在这之前我们先将这个创建书签的链接加到主页菜单上,并且加上一点限制,让它只能被登录用户看到。

限制只允许登录用户使用

我们在这里限制只有登录用户可以提交书签。打开 templates/base.html文件并加入下面的代码(新增的部分显示为黑体):

<div id="nav">
    <a href="/">home</a> |
    {% if user.is_authenticated %}
      <a href="/save/">submit</a> |
      <a href="/user/{{ user.username }}/">
        {{ user.username }}</a> |

      <a href="/logout/">logout</a>
    {% else %}
      <a href="/login/">login</a> |
      <a href="/register/">register</a>
    {% endif %}
  </div>

你有没有注意到,页面上只有登录用户可以看到提交书签的链接?我们不希望匿名用户提交书签。因为书签必须属于某个具体存在的用户。实际上有很多方法可以限制匿名用户提交书签信息,我们先介绍两个。

就像你看到的那样,我们在模板页面中调用 request.user.is_authenticated()方法来判断用户是否已经登录,所以我们可以把这个方法用到bookmark_save_page 视图函数中通过if语句先判断用户是否已经登录:

if request.user.is_authenticated():
  # Process form.
else:
  # Redirect to log-in page.

这种限制某个页面只有登录用户可以方法的功能非常通用,所以Django为此提供了一种快捷的方式,这种方式可以方便的实现我们想要的功能。让我们来修改一下bookmark_save_page函数:

from django.contrib.auth.decorators import login_required
@login_required
def bookmark_save_page(request):

我们要做的就是这些,首先从 django.contrib.auth.decorators 导入 login_required 方法。然后再我们的视图函数中使用它。这种新的语法方式看起来比较新,我们称之为装饰器语法。一个Python装饰器就是一个修改其他函数的函数。在这里我们用login_required装饰器来修改bookmark_save_page函数。这个装饰器首先检查用户是否登录,如果登录就允许其调用视图函数。

这里还有一些细节要澄清,login_required 函数是怎么知道我们的登录URL是什么的?确实情况下,它假定这个链接在/accounts/login/下,如果我们要修改这个缺省值需要修改一个名为 LOGIN_URL变量的值。这个变量位于django.contrib.auth下。我们需要在settings.py文件的末尾加入下面的代码:

import django.contrib.auth
django.contrib.auth.LOGIN_URL = '/login/'

现在你先登出当前账户让后直接输入下面的链接 http://127.0.0.1:8000/save/。你会发现你被重定向到登录页面。

注意:在Django1.0中你只需要在settings.py文件中增加一行LOGIN_URL = '/login/' 就可以了,其他的都不需要。


通常情况下在用户提交了书签之后就给用户展示其所有的书签列表,我们在前面的章节中创建了一个简单的页面显示一个用户所有的书签,下一节我们将对这个页面做一点改进,并增加一个新的页面显示某个标签下所有的书签。

浏览书签的方法

书签浏览是我们应用程序的核心功能,因此为用户多种方法来浏览和分享书签是非常重要的。尽管从业务上讲我们要提供多种浏览书签的方法,但是从技术上讲显示书签列表的方式是一样的:

  • 首先我们通过Django的数据模型API得到书签列表。

  • 然后我们使用模板技术把书签列表展示到页面上。

尽管不同页面上显示书签列表的方式从细节上看有些区别,但大体上是相似的。我们把列表中每个书签显示为一个链接,其后跟随用户信息和标签。如果我们能在所有页面中重用一个页面模板将大大提高系统的重用性,前面介绍的模板继承功能就是完成这个的。本节我们还将学到Django模板系统提供的另一种机制,称之为模板包含(include)。

模板包含的概念很简单,你可以使用它将另一个模板中的内容包含到当前的模板中。就像你将另一个模板中的内容复制到当前模板中一样。现在让我们看看它的使用方法,在templates目录下创建一个bookmark_list.html文件,输入下面的代码:

{% if bookmarks %}
  <ul class="bookmarks">
    {% for bookmark in bookmarks %}
      <li>
        <a href="{{ bookmark.link.url }}" class="title">
          {{ bookmark.title }}</a>
        <br />
        {% if show_tags %}
          Tags:
          {% if bookmark.tag_set.all %}
            <ul class="tags">
              {% for tag in bookmark.tag_set.all %}
                <li>{{ tag.name }}</li>
              {% endfor %}
            </ul>
          {% else %}
            None.
          {% endif %}
          <br />
        {% endif %}
        {% if show_user %}
          Posted by:
          <a href="/user/{{ bookmark.user.username }}/"
          class="username">
            {{ bookmark.user.username }}</a>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{% else %}
  <p>No bookmarks found.</p>
{% endif %}

如果你还记得我们是如何在用户页面中创建书签列表的,你会发现上面的代码和它非常相似。让我们来看看代码是如何工作的:

  • 代码首先判断bookmarks是否是空的,如果不是就在其中循环迭代每一个bookmark。否则就显示一个没有书签的消息。

  • 在for循环的外层,会首先输出每个书签的标题和链接。然后进入内层循环输出每个书签的标签。为了给程序提供更多的灵活性,我们增加了一个 show_tags变量,用于判断是否允许显示标签。

  • 最后我们增加一个 show_user 变量,判断是否允许显示用户,如果允许就提供一个到用户页面的链接。

我们这里定义了以show_开头的变量,一会儿你就会发现它们非常有用,因为这样开发人员可以通过给模板传递变量值来指定是否显示标签和用户信息。比如我们在用户页面中没有必要再显示一个指向用户页面的链接,因为我们已经在这个页面了。另外,我们在这里还加入了CSS classes标记,这样就可以在以后加上更多的页面修饰。

改进用户页面

现在我们可以在用户页面中使用上面这段代码了。我们需要将它包含到user_page.html页面中,所以请打开 templates/user_page.html 文件,并修改成下面这样:

{% extends "base.html" %}
{% block title %}{{ username }}{% endblock %}
{% block head %}Bookmarks for {{ username }}{% endblock %}
{% block content %}
  {% include "bookmark_list.html" %}
{% endblock %}


现在我们的代码变得非常简洁,我们只需要通过include标记将bookmark_list.html中的代码导入到当前的页面,就好像我们将其中的代码复制过来一样。这种方式极大的提高了重用性,我们可以在其他任何需要使用书签列表的地方重用这段代码,比如当我们想输出一个标签页面的时候。

在我们查看这个新页面之前还有两件事情要做,稍微修改一下用户视图函数和CSS样式单文件。在用户页面中我们希望显示书签列表,但是不希望显示用户信息。当前的 user_page 函数没有传递show_*变量给模板。由于在Django中为声明的变量的值默认为False,所以我们需要给show_tags赋值为Ture并传递给模板。请打开bookmarks/views.py文件并像下面这样修改:

from django.shortcuts import get_object_or_404
def user_page(request, username):
  user = get_object_or_404(User, username=username)
  bookmarks = user.bookmark_set.order_by('-id')
  variables = RequestContext(request, {
    'bookmarks': bookmarks,
    'username': username,
    'show_tags': True
  })
  return render_to_response('user_page.html', variables)

上面代码中的黑体字部分标出了本节中介绍的新内容,我们增加了show_tags变量,并且改变了获取用户信息的方式。这里我们用到了一种快捷方式,称为get_object_or_404,这个函数完成了原有代码一样的功能,首先它会尝试通过参数中提供的对象和属性字段来查找一个对象,如果找到了就返回这个对象,否则就返回一个404页面未找到异常。因为这种查找对象并在异常时抛出错误消息的任务非常普遍,所以Django提供了一种快捷方式来完成它。

另外我们还对获取书签列表的方式进行了改进,在这里我们没有采用all方法而是使用了order_by方法,这个方法可以对获得结果列表进行排序,方法的参数是字段名,Django根据参数的名称对结果集进行排序,如果字段前有一个-标记,说明要进行逆序排序,默认是正向排序。由于我们希望最新的书签显示在最前面,所以我们采用了逆序排序。

接下来对页面的显示效果进行一下改进,打开 site_media/style.css文件,加入下面的内容:

ul.tags, ul.tags li {
  display: inline;
  margin: 0;
  padding: 0;
}

这个CSS修饰告诉页面应该将标签显示为一行,这样看起来跟好看而且节省空间。现在启动开发服务器,打开浏览器输入下面的链接 http://127.0.0.1:8000/user/your_username/你将看到下面的页面:

创建标签页面

接下来我们将创建一个类似于书签页面的标签页。创建标签页不需要写更多额外的代码,我们只要把以前写的页面组合一下就可以了。

首先要给这个页面定义一个URL入口,打开urls.py 文件输入下面内容:

(r'^tag/([^\s]+)/$', tag_page),

这里的正则表达式和用户页面的有所不同,在那里我们要求用户名只能是字母,而这里对于标签我们要求可以是任何非空的字符串,因为用户可以在标签内输入注入&,+等这样的字符。在正则表达式中\s代表所有的空字符,我们在其前面加一个^代表除了空字符以外的任何字符,比如[abc]可以匹配a,b或者c但是[^abc]匹配的是除了a,b,c以为的任何字符。所以这里[^\s]正是我们想要的。

接下来我们创建一个tag_page函数,打开bookmarks/views.py 文件输入下面的内容:

def tag_page(request, tag_name):
  tag = get_object_or_404(Tag, name=tag_name)
  bookmarks = tag.bookmarks.order_by('-id')
  variables = RequestContext(request, {
    'bookmarks': bookmarks,
    'tag_name': tag_name,
    'show_tags': True,
    'show_user': True
  })
  return render_to_response('tag_page.html', variables)

你注意到没有,上面的代码和用户页面视图函数的代码非常相似,只不过这里我们要显示的是某个标签下的书签,而不是某个用户的书签。

最后我们需要给标签页面创建一个模板,新建文件templates/tag_page.html并输入下面的内容:

{% extends "base.html" %}
{% block title %}Tag: {{ tag_name }}{% endblock %}
{% block head %}Bookmarks for tag: {{ tag_name }}{% endblock%}
{% block content %}
  {% include "bookmark_list.html" %}
{% endblock %}


和视图函数的代码一样,这个标签页面模板和用户页面模板非常类似,我们只改了很少的部分,并且重用了bookmark_list.html页面就完成了一个功能。这一切都得益于Django提供的强大功能,我们最大限度的重用了代码,这一切就像搭积木一样简单。

在我们运行标签页面之前稍加改动一个下bookmark_list.html页面,给每个标签加一个指向标签页面的链接:

<ul class="tags">
  {% for tag in bookmark.tag_set.all %}
    <li>
      <a href="/tag/{{ tag.name }}/">{{ tag.name }}</a>
    </li>
  {% endfor %}
</ul>

这一改变会自动作用到用户页面上,这就是include的好处,我们不必对每个页面都进行修改。现在我们转到用户页面然后点击一个标签,你会看到类似下面的页面:


看起来很好,不是吗?现在我们的站点用户有了更多的选择来浏览书签。

创建一个书签

本章我们要实现的最后一个功能是标签云,标签云的概念是对目前系统中所有标签的一种虚拟化表示。标签名字的大小体现了标签下所包含的书签的多少,包含的书签越多,访问次数越多标签就越大否则就越小。

实现这个功能的关键点是找出所有的标签,以及每个标签下的书签数量。然后我们找出每个标签下包含书签的最大值和最小值。根据计算越靠近最大值的标签字体越大,越靠近最小值的标签字体越小。

现在打开 bookmarks/views.py 文件为标签云功能创建一个视图函数:

def tag_cloud_page(request):
  MAX_WEIGHT = 5
  tags = Tag.objects.order_by('name')
  # Calculate tag, min and max counts.
  min_count = max_count = tags[0].bookmarks.count()
  for tag in tags:
    tag.count = tag.bookmarks.count()
    if tag.count < min_count:
      min_count = tag.count
    if max_count < tag.count:
      max_count = tag.count
  # Calculate count range. Avoid dividing by zero.
  range = float(max_count - min_count)
  if range == 0.0:
    range = 1.0
  # Calculate tag weights.
  for tag in tags:
    tag.weight = int(
      MAX_WEIGHT * (tag.count - min_count) / range
    )
  variables = RequestContext(request, {
    'tags': tags
  })
  return render_to_response('tag_cloud_page.html', variables)

接下来让我们看看每行代码的含义:

  • 首先我们定义了一个MAX_WEIGHT变量并设置值为5,这表示标签的字体大小在0-5之间。

  • 接着我们得到了所有的标签列表。

  • 然后我们变量这个标签列表把标签中包含的书签数量放在一个临时变量count中,同时我们把书签的最大值和最小值放在max_count和min_count中。

  • 我们计算不同的max_count和min_count如果他们的差值为0就设置为1,这样避免了除数为0的情况。

  • 我们再次迭代标签列表并给每个标签的字体大小赋值。

  • 最后我们把计算后的标签列表传递给模板。

现在我们开始创建标签云的模板,新建一个名为 tag_cloud_page.html 的为恶极并输入下面的内容:

{% extends "base.html" %}
{% block title %}Tag Cloud{% endblock %}
{% block head %}Tag Cloud{% endblock %}
{% block content %}
  <div id="tag-cloud">
    {% for tag in tags %}
      <a href="/tag/{{ tag.name }}/"
      class="tag-cloud-{{ tag.weight }}">
        {{ tag.name }}</a>
    {% endfor %}
  </div>
{% endblock %}

这段代码很简单,它在标签列表中循环,然后给每个标签增加一个链接,同时为每个页面元素加上CSS class。

接下来我们把CSS描述信息加入site_media/style.css文件:

#tag-cloud {
  text-align: center;
}
#tag-cloud a {
  margin: 0 0.2em;
}
.tag-cloud-0 { font-size: 100%; }
.tag-cloud-1 { font-size: 120%; }
.tag-cloud-2 { font-size: 140%; }
.tag-cloud-3 { font-size: 160%; }
.tag-cloud-4 { font-size: 180%; }
.tag-cloud-5 { font-size: 200%; }

最后我们给这个新增的视图定义一个URL入口,打开 urls.py文件输入下面的内容;

(r'^tag/$',tag_cloud_page),

现在打开 http://127.0.0.1:8000/tag/链接,你应该看到下面的页面:

安全问题

在本章一开始,我们创建了一个Web表单用于收集用户输入数据,然后把这些数据存储到数据库中并页面上展示这些数据。由于我们的站点是完全公开的,任何人都可以注册一个用户并且向站点提交数据,所以我们需要提供一种保护机制以防止恶意数据的输入。

Web应用开发的黄金法则是“任何时候都不要相信用户的输入数据。” 任何时候你都应该对用户输入的数据进行校验,只有合法的数据你才能存入数据库并显示到页面上。本节我们将讨论Web应用程序的安全问题以及如何避免Web应用程序中通常的两个安全漏洞。

SQL注入

Web 应用程序中一中最常见的攻击是SQL注入,攻击者会利用技术手段控制SQL查询、从数据库中获取数据或者将恶意代码存储到数据库中。这是由于开发人员在使用SQL查询构造时没有使用特定的语法来避免特殊字符的输入。因为我使用的是Django封装的数据库访问API,Django数据库访问API会自动使用安全的SQL语法结构,我们可以很好的避免这种攻击。

跨站点脚本(XSS)

另一中攻击成为跨站点脚本攻击,使用这种攻击的方式是提交一段JavaScript脚本,当这段脚本在HTML页面上输出时,这段JavaScript代码就会自动执行从而取得当前页面的控制权并到去用户信息,比如cookies。为了避免这种攻击程序代码应该在输出HTML页面之前对信息进行校验以保证这些代码是无害的。Django没有提供自动校验的功能,不过它提供了一种方便使用的方法,通过它你可以在页面输出之前对信息进行安全检查。

让我们来看看如何使用这一功能,在我们的项目中对于书签的标签没有任何限制,只要是字符串就可以输入,假设用户输入下面的字符串:

<script>alert("Test.");</script>

那么在输出页面时会显示一个对话框。我们的应用程序允许输入这样的字符,并且会把它保存到数据库中。Django会自动将这样的特殊符号存入数据库。当你打开页面显示这个书签时你会看到一个弹出的对话框(Django1.0中会自动对HTML特殊符号进行转义),当然这或许无伤大雅,但是如果攻击者插入的是一段其他代码,比如窃取用户新的恶意代码那么情况就糟糕了。

幸运的是Django提供了一种称为模板过滤器的技术,你可以在输出页面是对于某些特殊字符进行处理。其中一种过滤器称为escape过滤器,这个过滤器会在输出页面时自动对HTML特殊字符进行转义。另外一个有用的过滤器称为urlencode,他会对URL链接字符串自动进行转义。

现在让我们在templates/bookmark_list.html页面中使用这个两个过滤器:

[...]
<a href="{{ bookmark.link.url|escape }}" class="title">
  {{ bookmark.title|escape }}</a>
<br />
{% if show_tags %}
  Tags:
  {% if bookmark.tag_set.all %}
    <ul class="tags">
    {% for tag in bookmark.tag_set.all %}
      <li><a href="/tag/{{ tag.name|urlencode }}/">
        {{ tag.name|escape }}</a></li>
    {% endfor %}
    </ul>
  {% else %}
    None.
  {% endif %}
  <br />
{% endif %}
[...]

这种语法结构类似于Unix/Linux中的管道符号语法,一个“|”后是过滤器,这个过滤器实际上是一个Python函数,它表示"|"符号前面的内容是后面过滤器函数的参数,当程序执行到这里时,会先通过过滤器将“|”前面的内容进行运算然后将返回值输出到页面。在上面的代码中tag.name会被当作参数传给escape过滤器函数,然后将转义后的结果返回到页面,这样就避免了跨站点脚本攻击的可能。

escape过滤器会将HTML符号<和>转换为&lt; 和&gt;,这样当浏览器遇到&lt;时会将它显示为<符号。

现在我们用相同的方法修改一下 templates/tag_page.html页面:

{% extends "base.html" %}
{% block title %}Tag: {{ tag_name|escape }}{% endblock %}
{% block head %}
  Bookmarks for tag: {{ tag_name|escape }}
{% endblock %}
{% block content %}
  {% include "bookmark_list.html" %}
{% endblock %}

和 templates/tag_cloud_page.html页面:

{% extends "base.html" %}
{% block title %}Tag Cloud{% endblock %}
{% block head %}Tag Cloud{% endblock %}
{% block content %}
  <div id="tag-cloud">
    {% for tag in tags %}
      <a href="/tag/{{ tag.name|urlencode }}/
        class="tag-cloud-{{ tag.weight }}">
          {{ tag.name|escape }}</a>
    {% endfor %}
  </div>
{% endblock %}

我们无需修改用户注册页面,因为在那里我们已经限制用户准输入字母和下划线,这不会有问题,如果你不记得了,可以参考bookmarks/forms.py 文件和前面的章节。

总结

到此我们将结束本章的内容。本章我们给书签共享程序增加许多重要的功能,同时学习了许多Django中的新功能。开始,我们创建了一个数据模型用于存储标签信息,然后我们由创建了一个表单用于用户提交书签信息,最后我们创建了用于浏览书签的页面。

下面是本章中介绍的Django功能的一个总结:

  • 在数据模型中多对多关系通过models.ManyToManyField表示。Django会自动创建数据模型之间的关系以及访问数据集合的属性。

  • 我们通过在数据模型类中增加 __str__ 方法的方式来定义一个打印数据模型时的输出内容。这个方法没有参数,它的返回值就是你数据模型输出的信息。

  • 我们学习了如何自定义表单字段的属性,对于widget字段,我们可以将HTML元素的属性封装成字段对象,作为参数传递给它。

  • 数据模型对象的objects属性提供了一个非常有用的方法称之为get_or_create。这个方法会根据给定的参数从数据库中查找一个对象,如果找不到就创建一个新的对象。

  • 为了限制只有登录用户可以访问模型页面,我们使用了 login_required 装饰器。你还需要重装保存在LOGIN_URL变量中的登录路径。

  • include模板标签的功能可以使我们高效的重用页面代码。

  • 你可以在模板中使用escape和urlencode过滤器来防止页面上的跨站点攻击。escape对数据中的HTML特殊字符进行转换,urlencode会将数据转换为一个合法的URL。

在下一章,我们会使用JavaScript和Ajax技术改进应用程序。这章将包含很多有趣的技术和功能,我们将学到更多的内容,跟着我一起继续吧。