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技术改进应用程序。这章将包含很多有趣的技术和功能,我们将学到更多的内容,跟着我一起继续吧。




No comments: