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




Tuesday, December 16, 2008

用户注册与管理

介绍


几乎每个Web应用程序中都存在用户注册和账户管理功能,用户在发布信息或者共享内容之前需要进行身份验证。在在线论坛和交友网络中用户账户也是必不可少的。因此,这一章我们将介绍用户账户的注册和管理功能,以及Django提供的身份管理系统中的高级功能。

本章我们将学习一下内容:


  • 创建一个登录页面。




  • 提供退出登录功能。




  • 创建一个注册表的。




  • 提供可以让用户自行修改个人信息的功能。




在介绍以上内容的过程中我们将学习Django中的两个重要功能:



  • 模板继承。




  • 表单库。






会话验证




上一章中我们学习了用户数据模型以及如何把用户信息保存到数据库中。实际上用户数据模型是Django提供的一个完整的用户认证管理功能的一部分。用户认证管理系统位于django.contrib.auth包下。在我们通过django-admin.py 创建一个项目的时候这个模块已经被缺省安装了。


你可以再确认一下你是否安装了这个模块,打开settings.py文件查看INSTALLED_APPS变量,这个变量包含了你创建的项目中所有的应用程序,你也许还记得,为了把我们创建的bookmarks应用程序增加到项目中,我们曾经修改过这个文件。最终的代码差不多是这个样子:


    INSTALLED_APPS = (

     'django.contrib.auth',

     'django.contrib.contenttypes',

     'django.contrib.sessions',

     'django.contrib.sites',

     'django_bookmarks.bookmarks',

    )


将来在你需要在项目中增加新的应用程序的时候,你只要在 INSTALLED_APPS 变量中增加新的包就可以了。根据应用程序的需要你可能还需要通过 python manage.py syncdb 命令将新的数据模型同步到数据库中。这样你就在当前项目中激活了这个应用程序。

在我们开始之前,先来快速的浏览一下用户认证管理系统都提供了哪些功能:

  • 用户:提供了一个用户数据模型,这个模型中包含了应用程序中通常需要包含的用户信息字段。

  • 许可:Yes/No 标记指出是否允许一个用户访问某个功能。

  • 组:提供了一个数据模型,这个模型用户包含一个或多个拥有相同许可的用户。

  • 消息:提供向用户展示信息或者错需消息的功能。

本章我们只使用用户管理功能,其他部分将在以后的章节中介绍。

创建登录页面

在我们分析用户数据模型的时候,我们已经注意到其中包含了一个用户账户。这个账户是在创建项目的时候建立的。所以现在的问题是,“如何登录这个账户”。

这里不得不提到会话控制系统,这是一个web的底层系统(就像PHP中提供的会话控制系统)理解其它比较抽象。在使用会话控制系统时要非常消息,任何一个小小的失误都可能给整个系统带来安全风险。不过庆幸的是,Django已经给我们提供了一个非常完善的会话控制系统,我们不必再为如何管理用户会话和密码而担心。Django已经实现了这些功能,我们只要直接使用就可以了。

为了介绍我们项目中的会话系统,让我们从用户登录页面开始。首先,你需要在urls.py中添加一个URL入口。打开urls.py文件加入新的URL之后文件大概是这样:

from django.conf.urls.defaults import *

from bookmarks.views import *

urlpatterns = patterns('',

(r'^$',main_page),   

(r'^user/(\w+)/$',user_page),        

(r'^login/$','django.contrib.auth.views.login')

)


这个新增的的URL和以前我们开到的略有不同。我们没有给这个URL指定一个具体的视图函数,而是将Python模型的路径作为字符串代替了视图函数。这是一种便捷当时,通常用于你要引用的函数不在当前的项目目录中。Django会自动把我们需要的函数导入到当前项目中。这个路径的开始部分django.contrib代表的是一个Python包,其中包含了许多Django的扩展。我们的用户认证管理系统就包含在这个包中。 django.contrib.auth.views 这个模块中包含了很多与用户会话管理相关的视图函数,在这里我们只使用了login函数,如函数名字所示,这个函数用于控制用户登录请求。但是如果要看到这个页面,我们需要写一个登录页面模板。

我们需要在registration目录下为login函数创建一个login.html模板。在加载这个模板的时候我们会给它传一个表单对象作为参数,在我们创建用户注册页面的时候会详细介绍什么是表单对象,现在我们只要记住这个对象的名字为form,并且包含以下属性form.username, form.password 和form.has_errors。在页面打开的时候前两个属性会创建用户名和密码文本框,form.has_errors是一个布尔值,如果在提交表单后登录失败,那么这个值为true。

接下来我们需要在templates目录下创建一个文件夹registration,然后再创建一个文件 login.html并输入一下内容:

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Django Bookmarks - User Login</title>

</head>

<body>

<h1>User Login</h1>

{%if form.has_errors%}

 <p>Your username and password didn't match.

        Please try again.</p>

{%endif%}

<form method="post" action=".">

      <p><label for="id_username">Username:</label>

        {{ form.username }}</p>

      <p><label for="id_password">Password:</label>

        {{ form.password }}</p>

      <input type="hidden" name="next" value="/" />

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

    </form>

</body>

</html>

上面的代码包含了输入用户名和密码的文本框以及一个提交按钮,隐藏于<input type="hidden" name="next" value="/" /> 包含了,如果成功登录后跳转的页面,这里我们将页面重定向到主页面。

现在一切都准备好了,打开浏览器输入下面的地址http://127.0.0.1:8000/login/,你应该看到类似下面的页面:


还记得在前面第二章中我们创建项目的时候曾经创建过一个用户吗?用那个用户信息进行登录,你应该可以成功登录主页面,然后再试试输入错误的用户名和密码,你应该能够看到一条提示信息(注意如果你使用的是Django1.0那么{%if form.has_errors%}这部分是不会工作的,你应该将它改成{%if form.errors%})。

好了,现在是时候给我们的主页增加些功能了,我们希望在主页上显示当前登录的用户是谁?跟着我修改main_page.html文件:

<html>

  <head>

    <title>Django Bookmarks</title>

  </head>

  <body>

    <h1>Welcome to Django Bookmarks</h1>

    {% if user.username %}

      <p>Welcome {{ user.username }}!

        Here you can store and share bookmarks!</p>

    {% else %}

      <p>Welcome anonymous user!

      You need to <a href="/login/">login</a>

      before you can store and share bookmarks.</p>

    {% endif %}

  </body>

</html>

在上面的模板文件中,模板文件假定有一个user对象参数,其中保存了用户数据信息,如果这个对象的用户名不为空,则说明用户已经成功登录,则显示欢迎信息,否则显示一个信息提示用户需要登录,并给出登录的链接。好了,现在我们有了这个模板,下面要做的事情是修改一下主页的视图函数,打开bookmarks/views.py 文件,修改函数为一下内容:

def main_page(request):

  template = get_template('main_page.html')

  variables = Context({ 'user': request.user })

  output = template.render(variables)

  return HttpResponse(output)

上面的代码加载了主页面并且把一个user对象作为参数传给它,获得user对象的方法是通过 request.user,我们将在后面介绍这种获得user对象的方法 。重新加载页面将显示下面这样更有好的欢迎页面:


你可能已经注意到了,像这种加载模板页面;创建变量;把变量传给模板页面的工作在Django中使用非常频繁,于是Django提供了一种更快捷的方式,让我们重写一下bookmarks/views.py文件:

from django.shortcuts import render_to_response

def main_page(request):

  return render_to_response(

    'main_page.html',

    { 'user': request.user }

  )

 django.shortcuts包中的render_to_response 方法简化了以上步骤,这个方法有两个参数模板文件名和包含传给模板文件的变量的字典对象,返回值是一个repsonse对象,这样你通过一行代码就完成了上面多行代码完成的工作。

request.user 返回的对象和我们前面介绍的User对象是一个类型,我们已经知道了User对象的一些字段属性,下面让我们来看看其中包含哪些方法:

  • is_authenticated()方法返回一个布尔值,用于表明用户是否成功登录。

  • get_full_name()方法返回用户的全名,姓和名之间用空格隔开。

  • email_user(subject, message, from_email=None)方法会给用户发一封邮件。

  • set_password(raw_password)设置当前用户密码。

  • check_password(raw_password)方法用于检查参数内提供的密码是否与用户的密码匹配。

这些方法通过名字就可以看出他们的用途,这里需要解释一下的是 set_password方法,为什么要单独提供一个方法来设置用户的密码呢,直接设置User对象的密码字段属性不行吗?要回答这个问题,我们需要看一下当前用户密码的内容。打开控制台,输入以下命令:

>>> from django.contrib.auth.models import User

>>> user = User.objects.get(id=1)

>>> user.password

                'sha1$e1f02$bc3c0ef7d3e5e405cbaac0a44cb007c3d34c372c'


根据你设置的密码,你得到的密码字符串可能和我的不同,但是不管怎样,他看起来是一连串像是随机数的字符。到底是怎么回事,其实为了安全起见,Django并没有将你的密码存储为纯未必字符串,而是在数据库中存储了你的密码的哈希值,你很难破解这个哈希值的内容,不过通过Django你一样可以验证密码。记住,我说过Django提供的会话管理系统避免了很多错误,密码存储就是其中之一,Django把这一切做得很好。你需要记住的就是当你要修改密码的时候调用set_password方法,而不要直接访问User对象的password属性,这个方法会自动把你的密码转换为哈希值。

提供注销登录功能

现在我们已经创建了登录页面,接下来需要为用户提供注销登录的功能。当用户点击 logout/链接的时候,用户将能够注销登录并返回主页面。

为了实现这个功能,我们首先要在 bookmarks/views.py中增加一个视图函数:

from django.http import HttpResponseRedirect

from django.contrib.auth import logout

def logout_page(request):

  logout(request)

  return HttpResponseRedirect('/')

我们导入了django.contrib.auth模块,并利用其中的logout方法使用户会话失效,接下来调用HttpResponseRedirect对象将页面重定向到主页,这个对象的参数是一个你要重定向的URL。

现在要做的事情是给这个视图函数配置一个URL入口,打开 urls.py文件输入以下内容:

urlpatterns = patterns('',

  (r'^$', main_page),

  (r'^user/(\w+)/$', user_page),

  (r'^login/$', 'django.contrib.auth.views.login'),

  (r'^logout/$', logout_page),

)

好了,现在打开浏览器,先登录然后点击logout链接http://127.0.0.1:8000/logout/,这时候你应该退出当前登录并以匿名用户的身份返回到主页面。为了在页面上增加这个logout链接,我们必须修改以前的页面模板,即使目前我们只要修改很少的页面,但是随着页面的逐渐增加,为了增加一个功能而不得不修改很多页面就变得很麻烦了,因此我们将学习Django中另一个有用的功能,模板继承。

改进模板结构

目前为止我们已经创建了三个模板页面,他们都存在相同的页面结构,唯一区别是标题和内容的不同。要是我们能够把相同的部分提取出来放在一个单独的文件中,那样在需要修改这些相同部分的时候我们只有修改一个文件就可以了。

幸运的是,Django已经为我们提供了这样的功能,成为模板继承。模板继承的设计原理很简单,我们提取系统中结构相同的元素,然后把他们放在一个基础(base)模板中,然后我们在这个基础模板中定义一些(blocks),这些块可以被继承他的子模板(child)修改。然后我们就可以定义一些子模板来继承(extends )这个基础模板,并根据子模板的需要修改这些块的内容。这种设计非常类似于面向对象编程语言中的对象继承。

好了,现在让我们将这一技术应用在当前的项目中。我们在templates目录下创建一个名为base.html的文件,并输入如下内容:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

  <title>Django Bookmarks | {% block title %}{% endblock %}</title>

</head>

<body>

  <h1>{% block head %}{% endblock %}</h1>

  {% block content %}{% endblock %}

</body>

</html>


在我们刚才创建的模板中使用了一个新的标签 block,这个标签用于定义一个段落,这个段落可以被继承他的子模板修改。我们创建的模板中包含三个段落标签,一个用于定义页面标题,其他的两个是定义页面的主题和页面内容段落。

现在我们看看如何使用新的标签,打开 templates/main_page.html 文件用下面的代码替换原来的内容:

{% extends "base.html" %}

{% block title %}Welcome to Django Bookmarks{% endblock %}

{% block head %}Welcome to Django Bookmarks{% endblock %}

{% block content %}

  {% if user.username %}

    <p>Welcome {{ user.username }}!

      Here you can store and share bookmarks!</p>

  {% else %}

    <p>Welcome anonymous user!

      You need to <a href="/login/">login</a>

      before you can store and share bookmarks.</p>

  {% endif %}

{% endblock %}


上面代码的第一行,我们使用了extends标签来继承base.html,这表明main_page.html是base.html的子模板。他继承了base.html中的所有元素并根据需要修改段落(block)中的内容。重新定义子模板中的块和在基础模板中定义块是一样的, main_page.html 中不再包含基本的HTML结构,它只是重新定义了它希望重用的部分。

利用同样的方法,我们再将templates/user_page.html页面也重写一下,让他继承基础页面(base.html):

{% extends "base.html" %}

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

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

{% block content %}

  {% if bookmarks %}

    <ul>

      {% for bookmark in bookmarks %}

        <li><a href="{{ bookmark.link.url }}">

          {{ bookmark.title }}</a></li>

      {% endfor %}

    </ul>

  {% else %}

    <p>No bookmarks found.</p>

  {% endif %}

{% endblock %}

user_page.html中的变化和main_page.html中的类似,我们重新定义了需要动态改变的内容。最后我们来修改一下templates/registration/login.html页面,让他也继承base.html页面:

{% extends "base.html" %}

{% block title %}User Login{% endblock %}

{% block head %}User Login{% endblock %}

{% block content %}

  {% if form.has_errors %}

    <p>Your username and password didn't match.  

       Please try again.</p>

  {% endif %}

  <form method="post" action=".">

    <p><label for="id_username">Username:</label>  

      {{ form.username }}</p>

    <p><label for="id_password">Password:</label>  

      {{ form.password }}</p>

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

    <input type="hidden" name="next" value="/" />

  </form>

{% endblock %}

现在我们的站点有了统一的基础模板,是时候改进一下站点的可用性和外观了。我们先从给站点页面添加CSS修饰开始。CSS文件和图片属于静态文件,Django没有提供对这些的文件的支持,在最终的产品环境中这些文件的维护由web方服务器负责。因为我们目前使用的是开发服务器,一个临时的解决方案是创建一个工作区来维护这些静态文件。

打开urls.py文件,输入以下内容:

import os.path

from django.conf.urls.defaults import *

from bookmarks.views import *

site_media = os.path.join(

  os.path.dirname(__file__), 'site_media'

)


urlpatterns = patterns('',

  (r'^$', main_page),

  (r'^user/(\w+)/$', user_page),

  (r'^login/$', 'django.contrib.auth.views.login'),

  (r'^logout/$', logout_page),

  (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',

    { 'document_root': site_media }),


)

我们刚才定义的入口将 site_media 目录下所有的URL绑定到django.views.static.serve视图函数上。与前面定义URL入口的方式有所不同,这个新增的部分出现了第三个参数,一些视图函数可以有附加的可以选字典对象参数。我们通过这种方式告诉Django静态文件存储的位置。

接下来在项目目录下创建一个site_media文件夹,然后在其中创建一个空的style.css文件。现在我们修改 templates/base.html 文件将新增的CSS加入到base.html文件中,并创建一个导航菜单:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>

  <title>Django Bookmarks |

    {% block title %}{% endblock %}</title>

  <link rel="stylesheet" href="/site_media/style.css"

    type="text/css" />


</head>

<body>

  <div id="nav">

    <a href="/">home</a> |

    {% if user.is_authenticated %}

      welcome {{ user.username }}

      (<a href="/logout">logout</a>)

    {% else %}

      <a href="/login/">login</a>

    {% endif %}

  </div>


  <h1>{% block head %}{% endblock %}</h1>

  {% block content %}{% endblock %}

</body>

</html>

现在我们需要调整一下导航菜单的位置,打开style.css样式文件,并输入下面的内容:

#nav {

  float: right;

}

现在在看一下页面,你会发现大部分页面都满足了我们的需求,只是当你打开user_page视图的时候,你会发现,无论你是否登录页面上都会显示一个login的链接。这是因为在base.html中if语句判断user对象是否已经验证登录。但是我们并没有给user_page.html传递user对象,有两个办法可以解决这个问题:

  • 编辑 user_page 视图函数,给他传递一个user对象。

  • 使用RequestContext对象,这个对象和Context对象稍有不同,它会自动加user对象(还有其他的一些对象)传递给模板。为了实现对象的传递,RequestContext对象的第一个参数是request,一个包含模板变量对象的字典对象作为它的第二个参数。

我们将采用第二种方法,因为这种方法更清晰,而且在将来我们可能要给每个页面传递user对象。而且这种方法也利于我们提前通用代码并减少代码录入。

我们将修改 main_page 和 user_page函数,让他们使用RequestContext对象,打开 bookmarks/views.py文件,找到 main_page 函数并把它改成下面这样:

from django.template import RequestContext

def main_page(request):

  return render_to_response(

    'main_page.html', RequestContext(request)

  )

如你所见,我们不再将 request.user 作为变量传递给模板,我们改用RequestContext对象来管理这一切。现在用同样的方法重写 user_page 函数:

def user_page(request, username):

  try:

    user = User.objects.get(username=username)

  except:

    raise Http404('Requested user not found.')

  bookmarks = user.bookmark_set.all()

  variables = RequestContext(request, {

     'username': username,


     'bookmarks': bookmarks

  })

  return render_to_response('user_page.html', variables)

现在整个代码看起来更紧凑了,虽然我们花了一些时间重构代码,但是从长远来讲新的代码结构是由好处的。

目前为止,我们为项目提供了登录和注销功能,在下一节中我们将讲述用户的注册功能。

用户注册

我们在创建项目的时候增加了第一个账户,然后站点的访问者希望能够创建自己的账户。如今几乎所有的社区站点都提供用户注册的功能,本节我们将创建一个表单用于提供用户注册功能,并通过它学习Django是如何控制表单的输出和处理的。

Django表单

在Web应用程序中创建,验证和处理表单是再平常不过的事情了。Web应用程序通过用户输入表单来收集整理数据,所以Django提供了一套函数库来处理这些任务。在Django0.96中的newforms库,在Django1.0中被替换为forms库,为了避免版本升级过程中带来的大量改变强烈建议通过以下方式导入包:

from django import newforms as forms

通过这种方式当Django升级为1.0之后你只要将包的导入方式改为一下这种方式就可以了:

from django import forms

我们介绍的这种包的导入方式将前面的包重新命名为forms,通过这种方式开发人员可以轻松的在Django的不同版本之间切换。

Django的forms包负责处理3中常归任务:

  • HTML表单的输出。


  • 用户输入的服务器端校验。

  • 当校验出现错误时,表单信息的重新显示。
这个库的工作模式与Django的数据模型工作方式类似。我们从定义一个代表表单的类对象开始,首先这个对象继承自forms.Form类。这个类中的属性对应了表单中的字段。forms对象提供了很多字段类型,从某种意义上将这和数据模型的属性类型类似。

当我们创建一个继承自forms.Form的对象,同时也继承了这个类中的方法,这些方法包括HTML代码的输出,访问输入数据的方法和校验表单的方法。下一节我们将通过开发用户注册表单来学习表单对象的方法。

设计用户注册表单

现在我们开始创建表单对象,首先在bookmarks文件夹下创建 forms.py.文件,然后输入以下代码:

from django import newforms as forms

class RegistrationForm(forms.Form):

  username = forms.CharField(label='Username', max_length=30)

  email = forms.EmailField(label='Email')

  password1 = forms.CharField(

     label='Password',

     widget=forms.PasswordInput()

  )

  password2 = forms.CharField(

     label='Password (Again)',

     widget=forms.PasswordInput()

  )

仔细看看代码你会发现定义表单类的方式和定义数据模型类的方式非常相似。我们让RegistrationForm类继承forms.Form类,所有的表单类都需要继承forms.Form类。接下来我们定义了表单包含的字段,forms中包含了很多字段类型,我们根据需要为每个字段指定不同的类型,如你所见,每个代表字段类型的方法都有若干个参数,他们都用于描述这个表单字段的特性:

  • label:用于在HTML中输出字段的标签。

  • required:代表这个字段是否属于必填字段,默认情况下所有的字段都是必填字段,除非你通过required=False设置为可选字段。

  • widget:这个属性用于控制当前这个字段在HTML中的显示方式,即使用那种窗口部件。比如上面的代码中我们将密码字段指定为CharField(这表示它是一个字符串),然后通过widget=forms.PasswordInput()参数指定在HTML中它应该显示为一个密码输入框。

  • help_text:为字段定义一个帮助信息,当输出HTML的时候会显示。

一下列出表单中使用的常用字段类型:



字段类型

描述

CharField
返回字符串

IntegerField
返回一个整数

DateField
返回Python的datetime.date 对象类型


DateTimeField
返回Python的datetime.datetime对象类型
EmailField
返回一个有效的Email字符串

URLField
返回一个有效的URL字符串



一下列出部分窗口部件类型:




部件类型

描述

PasswordInput
一个密码输入框

HiddenInput
一个隐藏字段

Textarea
一个文本输入域

FileInput
一个文件上传组件



我们可以通过一些练习来学习表单对象的API,请打开Python交互式控制台,并输入下面的内容:

$ python manage.py shell
>>> from bookmarks.forms import *
>>> form = RegistrationForm()

这样我们就有了一个RegistrationForm类的实例,现在让我们看看他是如何输出HTML的:

>>> print form.as_table()

执行上面的命令你会看到一串HTML代码,我们刚才定义的表单字段被放在一个HTML表格中,你还可以使用form.as_ul()和form.as_p()方法输出HTML,原理是一样的,他们分别把表单以ul(列表)和p(换行)的方式显示出来。

另外,你还可以通过这种方式查看某一个字段的HTML代码输出:

>>> print form['username']
  <input id="id_username" type="text" name="username" maxlength="30" />

现在我们已经知道了表单是怎么输出HTML代码的,现在看看表单是如何验证的,我们通过初始化RegistrationForm对象参数的方式创建一个用户注册表单对象:

>>> form = RegistrationForm({
...   'username': 'test',
...   'email': 'test@example.com',
...   'password1': 'test',
...   'password2': 'test'
... })
>>> form.is_valid()
        True

这里的form.is_valid()方法返回True说明我们输入的所有字段信息都是合法的,现在我们试试输入一些不合法的值:

>>> form = RegistrationForm({
...   'username': 'test',
...   'email': 'invalid email',
...   'password1': 'test',
...   'password2': 'test'
... })
>>> form.is_valid()
        False
>>> form.errors
        {'email': [u'Enter a valid e-mail address.']}

Django为我们完成了表单的验证,如果你没有为每个字段都赋值,你会得到类似的验证失败信息,因为缺省情况下所有的字段值都是必填的。如果你想知道一个表单对象中是否有数据,可以使用form.is_bound 方法,如果你尝试对一个不包含数据的表单对象进行校验,你会得到一个异常。你可以通过 form.data属性访问表单的数据,如果表单数据通过了验证,你可以通过form.clean_data(在Django1.0中这个属性是form.cleaned_data)方法已经校验过的数据。

现在你已经了解了forms.Form对象,我们还需要增强表单对象的验证,虽然现在的验证逻辑可以保证表单中的字段不能为空、必须输入有效的Email地址,但我们还想增加其他的验证规则:

  • 确保用户不输入了无效的用户名或者注册一个已经存的用户名。

  • 确保两次输入的密码是相同的。

让我们先从密码验证开始,因为这个相对简单一些。打开 bookmarks/forms.py文件在RegistrationForm类中增加以下代码:

def clean_password2(self):
    if 'password1' in self.clean_data:
      password1 = self.clean_data['password1']
      password2 = self.clean_data['password2']
      if password1 == password2:
        return password2
    raise forms.ValidationError('Passwords do not match.')

然我们来逐行解释以上代码:

  • 方法名是 clean_password2,自定义验证方法总是遵循clean加字段名  clean_fieldname 的规则。

  • 首先取得password1的值,如果用户输入的值是有效的,那么可以通过self.clean_data['password1']获得。password2的值用同样的方法获得。

  • 接下来我们判断两个密码值是否相等,如果相等就返回password2的值。

  • 如果两个密码值不相等,我们抛出一个 forms.ValidationError类型的异常。这个类型接收一个异常消息字符串作为参数。

给密码加上验证规则后,我们来给用户名增加验证规则,打开bookmarks/forms.py文件并导入下面的Python包:

import re
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist

其中re是Python正则表达式的库包,我们需要用它来确保用户名中没有非法字符。我们同时还导入了User用户模型,以便检验用户是否输入的是一个已经存在的用户名。最后我们导入ObjectDoesNotExist,这是一个异常类型用于当无法找到数据对象时抛出异常。

接下来将下面的代码加入到RegistrationForm中:

def clean_username(self):
  username = self.clean_data['username']
  if not re.search(r'^\w+$', username):
    raise forms.ValidationError('Username can only contain alphanumeric characters and the underscore.')
  try:
    User.objects.get(username=username)
  except ObjectDoesNotExist:
    return username
  raise forms.ValidationError('Username is already taken.').

我们再来看看每行代码都是什么含义:

  • 首先我们通过正则表达式^\w+$来检查用户名中是否有非法字符,我们要求用户名必须是字母数字或者下划线。我们在这里使用了re.search,如果它返回的是None,说明用户名和我们要求的模式不匹配。

  • 如果用户名不包含非法字符,那么我们通过User对象来查看现在是否已经存在一个同名的用户,如果没有(ObjectDoesNotExist)说明这是一个新的用户名,可以被使用,那么我们直接返回当前用户名,否则就抛出“用户名已经存在”的异常。

如果你愿意可以向我们前面做的那样,使用Python交互式命令行,在RegistrationForm中加入非法数据然后通过.errors看看是否按照预期显示了异常信息。你还可以给你的email 加上验证规则,比如一个email只能被一个用户名使用,只要按照clean_加字段名的方式就可以了。

我们现在已经有了注册表单,但是还缺少视图函数和模板。让我们先从创建视图函数开始,打开 bookmarks/views.py 文件,插入如下代码:

from bookmarks.forms import *
def register_page(request):
  if request.method == 'POST':
    form = RegistrationForm(request.POST)
    if form.is_valid():
      user = User.objects.create_user(
        username=form.clean_data['username'],
        password=form.clean_data['password1'],
        email=form.clean_data['email']
      )
      return HttpResponseRedirect('/')
  else:
    form = RegistrationForm()
  variables = RequestContext(request, {
     'form': form
  })

  return render_to_response(
    'registration/register.html',  
    variables
  )

以上代码完成两件事情:

  • 首先判断用户的请求类型是什么,如果是POST,说明用户是提交表单,那么就收集用户输入信息,如果一切正常就重定向到主页面。

  • 否则就将表单的HTML代码输出到 registration/register.html模板中。

我们通过 request.POST对象来就收用户输入的数据,这个对象是一个字典对象,它包含了用户输入的数据,我们通过调用RegistrationForm(request.POST)将它作为参数传给RegistrationForm对象,这样就可以将POST中的用户输入信息填充到我们定义的RegistrationForm对象中,然后我们就可以对表单进行验证,如果一切顺利将页面重新定向到主页。

你应该已经注意到了,我们这里创建用户时使用了 User.object.create_user方法,使用这个方法是因为它可以直接将我们输入的代码转换成哈希值,这使用起来更方便。

如果表单验证失败,表单信息还是会传给注册模板页面,并提供有用的帮助信息,告诉用户哪个字段验证失败了。

好了,现在我们来定义模板文件。创建一个新文件templates/registration/register.html加入如下代码:

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

你有没有注意到,视图函数和模板都非常紧凑简洁?这一切得益于Django强大的表单功能。在我们的模板文件中我们只是简单的使用 form.as_p显示用户数据,并增加了一个提交按钮。

最后,为了测试我们新增的用户注册功能,需要给他添加一个URL入口,打开urls.py文件输入如下内容:

(r'^register/$', register_page),

好了,完成了。现在启动开发服务器,打开浏览器输入下面的地址http://127.0.0.1:8000/register/ ,你应该看到用户注册页面,而且功能也如我们预期的那样,但是页面看起来有点凌乱,让我稍微修整一下,打开site_media下的style.css文件,输入下面的内容:

input {
  display: block;
}

现在页面看起来好多了:

现在试着输入一些非法的信息,你将会看到自动输出的错误提示信息。

好了,我们有了一个新的注册连接,你也许想把它加入到主页面的菜单中,那么打开templates/base.html为文件,加入如下内容:

<div id="nav">
  <a href="/">home</a> |
  {% if user.is_authenticated %}
    welcome {{ user.username }}
    (<a href="/logout">logout</a>)
  {% else %}

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

最后一件事,如果用户注册成功后系统显示注册成功的消息页面不是很好吗?这个页面中没有什么动态内容,就是一个静态的模板文件,对这种不需要处理动态内容的模板,Django在django.views.generic.simple包下direct_to_template 方法,好了,现在让我们实现这个页面。

创建一个显示注册成功的页面 templates/registration/register_success.html ,输入如下内容:

{% extends "base.html" %}
{% block title %}Registration Successful{% endblock %}
{% block head %}
  Registration Completed Successfully
{% endblock %}
{% block content %}
  Thank you for registering. Your information has been
  saved in the database. Now you can either
  <a href="/login/">login</a> or go back to the
  <a href="/">main page</a>.
{% endblock %}

接下来我们在urls.py文件中导入下面的包:

from django.views.generic.simple import direct_to_template

然后定义URL入口:

(r'^register/success/$', direct_to_template,
  { 'template': 'registration/register_success.html' }),

在上面的URL定义中我们在第三个参数总用字典对象定义了传输给视图函数的参数。最后修改register_page 视图函数,找到return HttpResponseRedirect('/')把它替换为:

return HttpResponseRedirect('/register/success/')

做完这些后我们测试一下,打开浏览器输入下面的链接:

http://127.0.0.1:8000/register/success/

当你成功注册一个用户后你会看到一个提示注册成功的页面。

账户管理

目前为止,我们已经实现了用户的会话管理和注册功能。现在我们希望用户可以更新自己的账户信息,比如密码和电子邮件。为了实现这一功能,我们可以:

  • 我们可以直接使用Django提供的通用账户管理功能。

  • 或者像定义注册表单那样,我们自己实现处理更新用户账户的功能。

我们将看到如何通过这两种方式实现,每种方式都有它的好处和坏处。很显然如果你自己实现这一功能,可操控性更强,不过你需要写更多的代码。另一方面,如果使用Django提供的视图更加方便快捷,不过损失的是灵活性。最终的决定取决于你的需求。

我将在下面总结 django.contrib.auth 包中提供的视图,每一个视图都需要一个模板,并且给这些模板传递一些变量。这些视图内部对输入信息进行维护,你不用操心。以下列出了 django.contrib.auth.views 中的视图函数:

  • logout:退出当前登录并显示一个模板页。

  • logout_then_login:退出当前登录并重定向到登录页面。

  • password_change:允许用户修改他们的密码。

  • password_change_done:用于在密码修改后的调用。

  • password_reset:允许用户重新设置密码,并通过邮件接收一个新的密码。

  • password_reset_done:用于重新设置密码后调用。

  • redirect_to_login:重定向到登录页面。

这些视图函数的使用方法和我们前面介绍的login函数类似,如果感兴趣可以参考Django的在线文档 http://www.djangoproject.com/documentation/0.96/authentication/

介绍完以上内容,我们准备结束本章的内容。现在你已经基本理解了Django用户身份管理系统,将来你也可以在自己的项目中实现用户会话管理和用户注册的功能了。

总结

本章你学到了如何使用Django提供的用户身份管理系统。这个系统提供的功能囊括了会话管理和账户管理。你使用这套系统在我们的标签分享应用程序中实现了用户注册和登录验证的功能。你还在这一过程中学到了Django的表单对象,并使用它创建了用户注册表单,并为这个表单增加了验证功能。你还学到了模板之间的继承功能,这是一个非常重要而又用的功能,利用它你可以建立一个结构清晰易于维护和重用的站点。

下面总结了本章学到的Django的功能:

  • 当前登录的用户对象User,可以通过 request.user 获得,它是通过HttpRequest对象提供给视图函数的。

  • 由于加载模板,装饰模板并把它封装到HttpResponse对象中是一种常用的功能,Django提供了 render_to_response的快捷方式,这个函数位于 django.shortcuts 包中。

  • 不要通过访问 user.password 属性直接设置用户密码,而是应该使用 user.set_passwor方法,这个方法会将密码转换成哈希值。

  • 要装饰一个表单对象,可以调用 as_table, as_p, 或者 as_ul方法。

  • 要向表单中填充数据,可以将数据放入一个字典对象中并在构造表单对象时传入这个字典对象。可以通过 is_valid()方法和errors 属性来检查表单数据的合法性。用户输入数据可以通过form.data属性获得,已经验证合法的用户输入数据可以通过 form.clean_data 属性获得(Django1.0中是form.cleaned_data)

  • 我们可以自定义表单字段的校验规则,只要按照 clean_fieldname的方式定义方法就可以了,记住如果校验成功方法返回校验后的字段值,否则要抛出 forms.ValidationError异常。

  • 可以通过HttpResponseRedirect对象将用户从一个视图重定向到另一个视图,这个对象的参数是要重定向的目的视图的路径。

下一章我们将给现在的应用程序增加更多的功能。我们将创建一个表单让用户自己将书签保持到站点上。这个表单将通过加“书签”的方式让用户对自己的标签内容进行整理。此外,我们还将利用书签集的方式让用户浏览数据库中的热门书签。下一章将包含很多有意思的内容,所以跟着我一起学习吧。

clean_data 和 cleaned_data

在我们使用Django的表单对象时我们会对表单对象进行验证,比如:
>>>python manage.py shell
>>>from pageage1 import MyForm
>>>form=MyForm({'username':'Eric','email':'test@test.com'})
>>>form.is_valid()
True

如果验证通过,你可以通过form.clean_data访问这些验证通过的数据,不过这是在Django0.96中的方式,在Django1.0中这个属性变成了form.cleaned_data:
...
>>>form.clean_data # Django 0.96
>>>form.cleaned_data # Django 1.0

Monday, December 15, 2008

软件开发的胡桃木钉锤

欧洲古老的传说中,对付魔鬼或者僵尸需要用胡桃木做的钉锤,狠狠的钉在他们身上就可以杀死魔鬼。《人件》一书中写道,“现在世界上某个角落的软件项目正在
走向失败”。套用一句名言:“成功的项目都是相同的,失败的项目各有各的不同”。没错,你的项目是不是想书中描写的史前巨兽正现在焦油坑里?项目需求不
清,或者用户的需求总是在变,在项目进行到关键时刻发现整个架构设计存在严重的缺陷,可能必须重新来过;项目组的人天天加班,人们感到沮丧,似乎看不到项
目的未来,天哪,这可恶的魔鬼。从今天开始让我们一起来探讨软件项目开发和管理的各个过程,一起打造一支胡桃木钉锤,杀死那些让项目陷入困境的
Zombie!

errors 和 has_errors

Django 升级到1.0后有不少的变化,今天先说一个。你以前如果使用过Django 自带的用户管理系统,你对他的表单对象和表单的属性一定不陌生,比如下面的页面:
<div style="margin-left: 40px;"> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Django Bookmarks - User Login</title>
</head>
<body>
<h1>User Login</h1>
{%if form.has_errors%}
<p>Your username and password didn't match.
Please try again.</p>
{%endif%}
<form method="post" action=".">
<p><label for="id_username">Username:</label>
{{ form.username }}</p>
<p><label for="id_password">Password:</label>
{{ form.password }}</p>
<input type="hidden" name="next" value="/">
<input type="submit" value="login">
</form>
</body>
</html>

在Django0.95中他可以很好的执行,但是如果使用1.0版本的,你会发现如果输错用户名或密码,你希望的错误信息不会出现了,原因是1.0中has_errors属性被替换为errors属性,所以,你只要把它们该一下就可以了。

Wednesday, December 10, 2008

创建一个社交书签分享应用程序




创建一个社交书签分享应用程序

介绍


前面的章节中我们我们创建了一个空的应用程序,并且学习了如何设置了数据库和启动开发服务器。接下来我们将开始创建我们的书签分析应用程序,并在这个过程中学习视图模型模板技术。

我们在这章中将介绍Django中的主要的组件。你将会学到如何在视图层创建动态页面,如何通过模型层将数据存储到数据库已经如何管理这些数据,并且还会学到如何通过模板技术简单的创建页面。在学习这些技术的过程中你将对Django这些主要的组件之间如何交互有一个更深的理解。后面的章节中我们会深入介绍这些组件,并且给我们的应用程序增加更多的功能。

本章讨论以下内容:

  • URLs和视图:创建主页
  • 模型:创建初始的数据结构
  • 模板:为主要创建一个模板
  • 结合以上内容创建一个用户页面



Django中的术语




Django 是一个MVC框架,但是在Django中控制器被称为“视图”,视图被称为“模板”。Django中的视图实际上是一个负责接收和维护数据的组件,而模板被用来向用户展示数据。因此Django也被称为MTV框架(MTV代表,模型 模板 视图)。虽然术语上不同,但是这并不改变Django是一个MVC框架,也不影响应用程序按照这一模式的开发过程。但是如果你以前使用过其他的MVC框架,最好还是把这些数据记下来以免造成混淆。




URL和视图:创建主页



当我们看到的主要程序的主页的时想到的第一件事是如何修改页面的内容。要创建我们自己的主页,第一件事通过URLs的方式在应用程序中增加一个入口,告诉Django在访问这个URLs的时候调用一个特定的Python函数。我们将写出这个Python函数让他显示我们自定义的主页。

创建主页

Django术语总的视图是一个负责响应页面请求并返回一个相应页面的Python函数。开始创建我们自己的主页之前,我们首先需要在我们的Django项目(project)中创建一个Django应用程序(appliaction)。你可以把应用程序想象成视图和数据模型的存储器。要创建一个应用程序请输入下面的命令:

$ python manage.py startapp bookmarks




创建应用程序的命令和创建项目的命令非常类似,用startapp 作为manage.py的第一个参数,第二个参数是应用程序的名称。

运行这个命令之后Django将在项目文件夹下创建一个新的文件夹名我bookmarks,这个文件夹下包含以下文件:

  • __init__.py:这个文件告诉Python,bookmarks是一个包
  • views.py:这个文件中将包含我们的视图逻辑
  • models.py:这个文件中将包含我们的数据模型

现在开始创建我们的主页的视图,打开bookmarks/views.py文件,输入以下内容:

from django.http import HttpResponse
def main_page(request):
output='''<html>
<head><title>%s</title></head>
<body>
<h1>%s</h1><p>%s</p>
</body>
</html> ''' %('Django Bookmarks',
'Welcome to Django Bookmarks',
'Where you can store and share bookmarks!')
return HttpResponse(output)


代码非常简短而且直白,让我们一行行的看看他们是什么意思:

  • 首先我们需要导入从django.http导入HttpResponse类,我们需要这个对象来完成页面输出的功能。
  • 我们定义了一个函数名我main_page,并且他只有一个参数名我request;这个参数中包含了用和输入和其他信息。比如:request.GET, request.POST和request.COOKIES,这些属性是Python中的字典对象,他们分别代表了request,response和cookie。
  • 接着我们创建了Html代码并把它们封装到HttpResponse对象中返回给客户端。

创建主页的URL

你应该还记得前面的章节中我们介绍如何创建一个项目时,曾经创建了一个urls.py文件。这个文件中包含了应用程序中所有的URL,并将每个URL映射到一个视图Python函数上。让我们来看看这个文件的内容并试着加入我们自己的内容:

from django.conf.urls.defaults import *

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
# Example:
# (r'^django_bookmarks/', include('django_bookmarks.foo.urls')),

# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),

# Uncomment the next line to enable the admin:
# (r'^admin/(.*)', admin.site.root),
)
正如你看到的那样,这个文件包含了从URL到Python视图函数映射的一个表,这个名为urlpatterns的表实际上是一个Python的元组,已经包含了一些被注释掉的例子代码,他们描述了URL和Django视图函数的映射关系。

这些URL语法你看起来应该很熟悉,因为它们实际上就是正则表达式,通过这种方式Django为你定义URL提供了最大的灵活性。我们将逐步学习如何使用这种URL语法,现在让我们把这些注释移调,并给我们的主页加一个URL入口:

from django.conf.urls.defaults import *
from bookmarks.views import *
urlpatterns = patterns('',
(r'^$',main_page),
)

现在,让我们再来看看这些代码的含义:

  • 首先将django.conf.urls.defaults下面的所有内容都导入进来,这个模块下包含了定义URL必须的函数。
  • 然后导入bookmarks.views下所有的内容,这里包含了我们的视图函数。
  • patterns用于定义URL表,现在我只有一个URL映射关系--从r'^$'到main_page。

在我们开始介绍视图的概念之前,最后一个需要解释的是正则表达式,如果你以前没有使用过正则表达式,那么这种语法会让你感觉很奇怪。在我们的代码中,这个正则表达式中包含一个原生的字符串,字符串中有两个字符^和$。r'' 是Python的语法结构,它用于定义一个原生字符串,如果Python解析器遇到这样的原生字符串的时候不会解析器中的反斜杠(/)和其他转码字符。这对于正则表达式是非常有用的,因为在这些表达式中经常包含反斜杠或者转码字符。

在正则表达式中^代表字符串的开始,$代表字符串的结束。所以$通常表示一个字符串不包含任何内容,是一个空字符串。这很符合我们主页URL的要求,因为主页通常是站点的跟节点,不包含任何内容。

Python针对re模块的有关文档相信介绍了正则表达式,如果你想深入了解正则表达式我建议你先阅读这些文档:

http://docs.python.org/lib/module-re.html

下面列表是对正则表达式语法的一个简单的索引:

符号/表达式匹配的字符串
.(点号)任意字符串
^(加字符号)字符串的开始
$字符串的结束
*重复0到多次
+重复1到多次
0或者1个重复
|A|B 表示A或者B
[a-z]任意的小写字符
\w任意字母或者下划线_
\d任意的数字

了,现在一切都准备好了,让我们看看最终的效果是什么样,启动服务器并打开浏览器输入:http://127.0.0.1:8000 你将会看到下面的页面。

恭喜你,你已经成功的场景了第一个Django视图。

在我们开始下一届内容之前,最好来了解一下这一切的背后是怎么回事:

  • 当用户向http://127.0.0.1:8000/发出请求时,Django会从urls.py文件中查找URL匹配的一个函数是什么,匹配的过程使用了正则表达式。
  • 当找到匹配的URL那么Django就调用它对于的那个Python函数,这个函数接收来自用户浏览器的请求,请求参数包含在request对象中,然后将返回页面封住如HttpResponse对象输出到浏览器。
  • 如果Django找不到URL,他会抛出一个404“页面错误”的异常。如果你输入下面的链接http://127.0.0.1:8000/does_not_exist/ 你就会看到上面提到的异常。值得一提的是Django提供了详细的调试信息,这在开发中是非常有帮助的,当然在你发布产品的时候你可以关闭调试选项来取消输出调试信息。
这种从URL到视图的映射方式为开发人员提供了极大的灵活性,它不像PHP中那样将URL限制为文件名,也不像mod_python那样将URL自动映射到一个Python函数上。在URL和函数之间如何映射这件事上开发人员有完全的控制权。尤其是在大型项目的开发中这非常有用,因为这些项目中URL和函数名经常会变更。

模型:创建一个初始数据模型

几乎每一个web2.0的应用程序都需要一个数据库来存储和管理数据。如今数据库引擎已经成为了web开发的基础。web应用程序为用户提供一个界面来输入和管理他们的数据,在程序后台通过数据库来存储这些数据。


在Django中你可以将视图当作是搜集和展示数据的组件,而模型用于存储和管理这些数据


在前面的章节中我们已经介绍了如何在我们的程序中配置数据库。本节我们将讲述如何将我们的用户帐号数据和标签信息存储到数据库中。如果你习惯于在数据库中直接使用SQL语句,你会发现Django中的数据访问方式稍有不同。简单的讲,Django将对数据库表的访问抽象为通过Python类来访问。开发人员将通过Python形式的API来存储访问和管理数据库中的数据。因此具备SQL知识有一定的帮助,但不是必须的。

我们最好是通过一个例子来讲解这部分内容,在我们的标签共享应用程序中,我们要在数据库中存储3中类型的数据:

  • Users (ID, username, password, email)
  • Links (ID, URL)
  • Bookmarks (ID, title, user_id, link_id)

每个用户的信息都要单独存储在Users表中。这个表中存储了用户的名词、密码和电子邮件。同理,每一个链接都要存储在Links表中,目前我们只存储链接的URL地址。

对于Bookmarks表,你可以把它当作是Users和Links表之间的连接表。当用户增加一个标签,那么首先要检查Links表中是否存在相同的URL,如果不存在就增加一个,同时User和Link之间的连接关系被保存在Bookmarks表中,并且还要保存用户输入的代表这个URL的标签名词。

要把以上设计转化为时间的Python代码,我们需要编辑bookmarks文件夹下的models.py文件,并在其中定义每一个类型。models.py文件用于描述每一个数据模型,在开始创建的时候它只有一条import语句。

链接表的数据模型

让我们从创建Links表的数据模型开始,因为它是最简单的一个。打开bookmarks/models.py文件,输入如下内容:

class Link(models.Model):
url=models.URLField(unique=True)

我们来看看这些代码是什么意思:

  • 首先导入models模块,它包含了定义模型必须的类。
  • 然后定义一个类Link,它继承自models.Model类,这个类是所有模型类的基类。并且这个类之包含一个字段url,它的类型是models.URLField,而且这个字段的值必须是唯一的。

models.URLField类型是Django提供的众多字段类型之一。下表是这些字段类型的一部分:

字段类型
描述
IntegerField
代表一个整数
TextField
一个大的文本字段
DateTimeField
日期时间格式的字段
EmailField
最大75个字符的email地址字段
URLField
最大200个字符的URL地址字段
FileField
一个文件上传字段
要使用这个字段我们首先需要在Django项目中将它激活。我们通过配置settings.py文件来完成,首先找到NSTALLED_APPS变量,然后将我们的应用程序名称(django_bookmarks.bookmarks)加入其中:

INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django_bookmarks.bookmarks',
)
然后执行下面的命令在数据库中创建Links表:

$ python manage.py syncdb

你应该记得我们在前面使用这个命令来创建Django自己的管理表。每次你增加新的数据模型的时候你都需要执行这个命令来向数据库添加新的表。

如果你熟悉SQL,你可能希望看看Django产生的SQL语句是什么,那么运行下面的命令:

$ python manage.py sql bookmarks

如果你使用的是不同的数据库引擎,输出的结果略有不同,对于SQLite3,输出的SQL是这样的:

BEGIN;
CREATE TABLE "bookmarks_link" (
"id" integer NOT NULL PRIMARY KEY,
"url" varchar(200) NOT NULL UNIQUE
);
COMMIT;

我们在执行上面的语句到底发生了什么?首先Django会分析我们的Link模型,就是那个我们定义的Python类,然后产生一个SQL CREATE 语句来创建一个名为bookmarks_link的表。这个表将存储Link类的实例。这里注意一点,Django自动给表增加了一个id字段,这个字段是Link表的主键,以便我们可以唯一的确定一个链接。

Django的Python数据库访问API不仅仅是创建数据库表这么简单,我们可以通过它向表中增加记录、修改记录、查询记录等等操作。我们可以通过Python交互命令控制台来看看这些API是如何使用的,请输入下面的命令:

$ python manage.py shell

这里和标准的Python交互控制台有两点不同,首先我们当前项目的路径被自动加入到Python的sys.path中,其次,它创建了一个特殊的环境变量为settings.py存储这个路径。所以,当你想通过一个命令行来与你的项目交互的话,请使用上面的命令。

接下来,我们导入要使用的模型模块:

>>> from bookmarks.models import *

要在数据库中增加一条link记录,我们需要实例化Link类,并调用它的save方法:

>>> link1 = Link(url='http://www.packtpub.com/')
>>> link1.save()
>>> link2 = Link(url='http://www.example.com/')
>>> link2.save()

要想将数据存储在数据库中,必须调用save方法,在调用这个方法之前你的数据实际上是在内存中,一但你关闭控制台刚才的数据就会丢失。在这里数据库表的字段变成了类的属性,如果你想看看刚才保存的数据或者修改数据,输入下面的命令:

>>> link2.url
'http://www.example.com/'
>>> link2.url = 'http://www.google.com/'
>>> link2.save()

如果想看看刚才保存的所有链接信息,输入下面的内容:

>>> links = Link.objects.all()
>>> for link in links:
... print link.url
...
http://www.packtpub.com/
http://www.google.com/

你可以可以通过Id来得到一个保存的链接信息:

>>> Link.objects.get(id=1)
<Link: Link object>

最后,如果你想删除一条数据,输入下面的内容:

>>> link2.delete()
>>> Link.objects.count()
1

输出结果为1,表示我们已经成功的删除了一条记录,现在只剩下一条链接信息。请注意以上的代码中我们没有使用一行SQL语句,所有这些操作都通过PythonAPI来完成,Django把执行SQL的这一过程封装成PythonAPI。这样做的好处是:

  • 你已经学会了Python,那么使用Django你不必学习另一门语言来处理数据库任务。
  • Django负责Python对象和数据库表之间的转换,你只要操作Python对象就可以,Django负责数据库的存储、查找和管理。
  • 你不必关心不同数据库中特殊的SQL语法,尤其是你需要从一个数据库切换到另一个数据库时。不管你用的是什么数据库,操作PythonAPI的方式是一样的。

用户数据模型

现在你已经熟悉了Django中的数据模型,是时候来创建用户对象了。幸运的是,Django为我们提供了一套通用的用户模型。这个模型包括了通常都会有的用户名、密码和电子邮件等等。这个名为User的模型位于django.contrib.auth.models包下面:

可以通过下面的命令来查看这个对象:

>>> from django.contrib.auth.models import User
>>> User.objects.all()
[<User: ayman>]

你一定还记得们前面章节中将数据库设置的时候,曾经创建了一个超级用户,Djangon把这个信息存储到了数据库中。你可以通过dir函数来查看User对象都有哪些属性和方法:

>>> user = User.objects.get(id=1)
>>> dir(user)

执行上面的语句后你会得到一长串信息,其中你一定会找到username, email 和 password属性。很不错,这个模型完全满足我们的需要,Django提供了我们所需的所有用户对象属性,我们直接使用这User模型不必再重新创建了。

标签数据模型

现在就剩下最后一个数据模型了,Bookmark模型。前面我们提到过这个模型,我们意识到这个对象用于连接User和Link模型。一个标签对应一个用户和一个链接,然而一个用户可以有多个标签,同时一个链接可能被多个用户设置为标签。从数据库的角度将,我们说user表和link表之间存在多对多的关系。但是,没有一个直接的方法来描述两个表之间的多对多关系,于是我们通过Bookmark与User和Link之间分别建立一对多的关系来解决这个问题。

首先,我们建立用户和书签之间的一对多关系,一个用户可以有多个标签,但是一个标签只能与一个用户关联。也就是对应一个具体的链接一个用户只能标记一次。

其次,我们建立连接和标签之间的一对多关系,一个链接可以有多个标签与之关联,比如有多个用户都标记了这个链接,但是反过来一个标签只对应一个链接。

现在我们有了两个单独的一对多关系,这样我们就可以在数据库中对其进行描述了。因此我们需要创建第三个表,bookmarks 表。这个表连接了user表和links表。bookmarks中的每一个表都包含一个指向user表的引用和一个指向links表的引用。在SQL中这种与外部表之间的引用字段成为“外键”。但是在Django中我们无需通过SQL创建这个中间表,我们通过创建bookmarks模型来实现。

下面的代码创建了Bookmark数据模型,你需要把它们加入到bookmarks/models.py文件中,对于以前没有接触过SQL的人,下面的代码看起来不是很清晰,随着不断的什么你会明白它的意义所在,下面是Bookmark数据模型的代码:

class Bookmark(models.Model):
title=models.CharField(maxlength=200)
user=models.ForeignKey(User)
link=models.ForeignKey(Link)

首先我们导入User模型,以便可以在Bookmark中建立引用。接着我们为Bookmark定义一个类。这个新的模型包含一个title字段,它的类型是文本型,然后是分别创建指向User和Link的引用。

按照Python的规约,所有的import语句都应该位于源文件的开始部分,当加入新的代码后,相关的import语句要位于新导入代码的前端,但是建议你最好把所有的import都放在源文件的前面。

加入上面的代码后需要执行manage.py syncdb命令来与数据库同步。让我们来看看Django输出的SQL语句有哪些:

$ python manage.py sql bookmarks

你将看到下面的输出信息,Django为我们创建了必要的表:

BEGIN;

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_link" (

"id" integer NOT NULL PRIMARY KEY,

"url" varchar(200) NOT NULL UNIQUE

);

COMMIT;

注意,Django自动给每个外键的_id之前加了一个前缀,并创建了表之间的外键关联。

现在数据模型已经建立好了,我们可以方面的存储和控制我们的数据,Django提供了一套优雅而直白的Python API 来吧Python对象存储到数据库中,这确实把开发人员从编写复杂的SQL工作中解脱了出来。

接下来我们将学习Django中另一个主要的组件技术,模板系统。通过模板系统我们可以非常容易的创建页面,并且利用本章学到的知识为用户创建一个标签列表。

模板:为主要创建一个模板

在本章一开始的部分我们创建了一个简单的主要,主页的HTML代码嵌入到了视图层的代码中,这样做害处很多:

  • 好的软件工程总是强调要将用户界面和业务逻辑分离,因为这样做重用性更高。然而将HTML嵌入到Python代码中的做法违背了这一原则。

  • 修改这种嵌入的代码需要具备Python知识,但是在很多开发团队中,要求页面设计人员理解Python是不现实的。

  • 维护Python代码中的HTML相当麻烦。比如HTML中的引号在Python字符串中需要转移,而且一大堆代码看起来也不清晰易读。

因此解决的办法是将HTML从Python的视图层中分离出来。幸好,Django为我们提供了一个方便的组件来做这件事,这就是模板系统。

这套系统的思想非常简单,我们不在视图层中嵌入HTML而是把它们分离出来放在一个单独的文件中,称为模板。这个模板中可以包含很多占位符,以便展示视图层中的动态数据。当页面输出的时候视图层会加载这个模板文件并将动态值传给这个模板,接着模板系统会逐个的将占位符替换为动态值。为了更好的理解这个概念,让我们将它应用到main_page中。

首先我们要在项目文件夹下创建一个名为templates的文件夹。接下来我们要告诉Django我们的这个模板文件夹templates的位置。所以,打开settings.py,文件并找到TEMPLATE_DIRS变量,把模板文件夹的绝对路径加入其中,如果你不想将模板路径硬编码在settings.py文件中,你可以设置相对路径,请加入下面这对代码:

import os.path

TEMPLATE_DIRS = (

os.path.join(os.path.dirname(__file__), 'templates'),

)

然后在templates文件夹下创建一个main_page.html文件:


<html>

<head>


<title>{{head_title}}</title>

</head>

<body>

<h1>{{page_title}}</h1>

<p>{{page_body}}</p>

</body>

</html>

上面的代码和我们在main_page函数中嵌入的代码非常类似,稍有不同的是我们使用了一种特殊的语法结构来标明我们希望视图层改变的部分,比如{{ head_title }}定义了一个变量head_title,它可以被视图层动态的改变,模板中的变量使用双括号包含。

现在我们看看如何结合视图使用模板系统,编辑bookmarks/views.py文件并加入下面的代码:

from django.http import HttpResponse

from django.template import Context

from django.template.loader import get_template

def main_page(request):

template=get_template('main_page.html')

variables=Context({

'head_title':"Django Bookmarks",

'page_title':"Welcome to Django Bookmarks",

'page_body':"Where you can store and share you bookmark"})

output=template.render(variables)

return HttpResponse(output)

和往常一样让我们逐行解释这段代码:

  • 我们从django.template.loader中导入get_template方法,这个方法通过导入一个文件来返回一个模板对象。

  • 为了给模板中变量赋值,我们声明了一个Context类型的variables变量,这个Context对象包含一个Python的字典对象。在这个字典对象中的的内容是模板对象中的变量名,每一个键的值是模板中变量的值。

  • 为了将模板中的变量替换成我们想要的内容我们使用了模板对象的render方法,这个方法接收一个Context对象参数。所以这里我们把variables变量作为参数传递给这个方法。

  • 最后我们将封装好的包含Html的输出内容交给HttpResponse返回。

如你所见使用上述方法比原来的做法更加简单而且清晰。我们无需在Python代码中嵌入HTML,相反把HTML代码放到一个单独的文件中时的程序更加清晰,结合Django模板系统使这一切变得更加容易。

Django的模板系统还提供了很多其他变量子集,比如用于条件判断的"if"变量,和用于循环的"for"变量,我们将在下面一节中逐步学习这些内容,并创建我们的用户页面。

结合所学的知识:创建一个用户页面

本章已经介绍了不少内容,比如视图、模型和模板的概念。本章剩下的部分我们将结合以上所学的内容创建一个新的页面,这个页面将显示一个用户的所有书签列表。

创建URL

这个新创建页面视图的URL的形式为:user/username,username代表我们要查看这个用户的所有书签。这个URL和我们以前所创建的有所不同,其中的部分内容是动态的。我们将利用正则表达式提供的强大功能来描述这个URL。打开urls.py文件输入如下内容:

from django.conf.urls.defaults import *
from bookmarks.views import *
urlpatterns = patterns('',
(r'^$',main_page),
(r'^user/(\w+)/$',user_page)
)

上面新增的正则表达式比前面介绍的要复杂一点。\w代表这里是字母或者下划线,+代表有一个或多个值。所以实际上,\w+代表这里可以是一个或多个任意的字母或者下划线。这样Diango就会根据我们正则表达式中的内容匹配相应的URL并执行相应的视图函数。

创建和视图

现在我们已经为视图函数定义好了URL入口,是时候创建视图函数了,打开bookmarks/views.py文件并输入下面的内容:

from django.http import HttpResponse,Http404
from django.template import Context
from django.template.loader import get_template
from django.contrib.auth.models import User
def main_page(request):
template=get_template('main_page.html')
variables=Context({
'head_title':"Django Bookmarks",
'page_title':"Welcome to Django Bookmarks",
'page_body':"Where you can store and share you bookmark"})
output=template.render(variables)
return HttpResponse(output)
def user_page(request,username):
try:
user=User.objects.get(username=username)
except:
raise Http404("Requested user not found")
bookmarks=user.bookmark_set.all()
template=get_template("user_page.html")
variables=Context({
"username":username,
"bookmarks":bookmarks})
output=template.render(variables)
return HttpResponse(output)

所有的视图函数看起来都差不多,不过还是让我们一起来看看有什么新的内容:

  • 和我们以前创建的函数不同,user_page函数多了一个username参数,还记不记得我们前面定义URL的时候有+w是放在一个括号中的,这种放在括号总的内容会被当作参数传递给视图函数。这个参数就是这里的username。

  • 我们通过User.objects.get方法来查找用户名为username的用户对象。实际上我们可以使用类似的方法查找所有表,主要这个字段的值是唯一的。如果找不到相应的用户,或者有重名的用户对象,这个方法会抛出一个异常。

  • 如果找不到相应的用户对象,我们使用Http404对象抛出一个404异常。

  • 要查询一个用户所有的书签信息,我们可以通过user对象的bookmark_set属性来获得。你不必自己创建这个方法,Django会根据模型之间的关系自动识别并创建相应的属性,好处是,比如你必须再关系SQL 链接查询(JOIN)是否会出错。

设计模板

我们前面创建的视图函数需要一个user_page.html页面模板,并向其传递了参数username和bookmarks变量。现在我们来创建这个模板,在templates文件夹下创建 user_page.html文件,并输入下面内容:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Django bookmarks --User {{username}}</title>
</head>
<body>
<h1>Bookmarks for User {{username}}</h1>
{%if bookmarks%}
<ul>
{%for bookmark in bookmarks%}
<li> <a href="{{bookmark.link.url}}">{{bookmark.title}}</a></li>
{%endfor%}
</ul>
{%else%}
<p>No bookmarks Found</p>
{%endif%}
</body>
</html>

这个文件的内容比以前的要复杂的多,你使用了“if”和"for" 变量来显示用户的书签信息。bookmarks对象是一个数组对象,所以我们不能直接在页面上显示它;首先我们要确定这个数组对象不为空,然后我们再迭代显示器中的内容。测试一个变量是否为空我们使用了下面的语法结构:

{% if variable %}
<p>variable 中包含数据.</p>
{% else %}
<p>variable是空的</p>
{% endif %}

“if”变量用于进行调解判断,如果判断值不为空则输出第一行内容,否认输出第二行内容。

在一个数组中迭代我们使用下面的语法:

{% for item in list %}
{{ item }}
{% endfor %}

如果要访问一个变量的属性,只要像访问pytohn对象属性那样就可以了:

{{ variable.attribute }}

结合上面的知识我们创建了user_page.html页面,首先我们判断bookmarks中是否为空,如果不为空,那么我们在其中迭代,并逐行显示用户的标签信息;如果为空,我们就打印一条提示信息。

现在打开浏览器并输入下面地址http://127.0.0.1:8000/user/your_username(把your_username替换成你数据库中实际的用户名),你将看到下面的页面:

我们的页面可以正常的现实,但是如你所见,现在没有任何书签数据,让我们动手加入一些,输入下面的命令:

$ python manage.py shell

为数据模型填充数据

首先创建user对象和link对象的实例:

>>> from django.contrib.auth.models import User
>>> from bookmarks.models import *
>>> user = User.objects.get(id=1)
>>> link = Link.objects.get(id=1)

注意现在user.bookmark_set是空的:

>>> user.bookmark_set.all()
[]

现在创建一个bookmark对象,让他关联上面的user和link:

>>> bookmark = Bookmark(
... title='Packt Publishing',
... user=user,
... link=link
... )
>>> bookmark.save()

现在再检查一下user.bookmark_set:

>>> user.bookmark_set.all()
[<Bookmark: Bookmark object>]

现在刷新一下原来的页面,你会看到下面的内容:

你可以自己增加一些数据试试。还有一个方法bookmark.user,你可以通过bookmark对象来获得user对象,这是Django自动创建的另一个方法,根据模型之间的对应关系,user和bookmark之间是一对多的关系,所以一个用户有多个书签,你可以通过user.bookmark_set获得一个用户的所有书签;而每一个书签只属于一个用户,所以你可以通过bookmark.user得到这个书签对应的用户是什么。

总结

在这一章中我们学习了Django中的3个主要组件:视图、模型和模板。我们创建了数据模型,以便可以通过它为我们的应用程序存储数据,然后创建视图和模板来展示这些数据。我们还学习了如何将URL映射到一个视图函数上,已经如何利用交互式命令行向数据库中增加数据一般检验我们的程序。

下面总结了本章中讲解的Django的知识:

  • 要在项目中创建一个应用程序,使用下面的命令:

    • $ python manage.py startapp <app-name>

  • 创建数据模型之后应该使用下面的命令来与数据库同步:

    • $ python manage.py syncdb

  • 要想查看Django输出的SQL内容,使用下面的命令:

    • $ python manage.py sql <app-name>

  • Django的数据模型提供了一系列的方法与数据库进行交互:

    • save将一个对象保存到数据库

    • objects.get方法获得一个唯一的数据对象。

    • objects.all方法获得所有的数据

    • delete方法从数据库中删除一条数据

  • 通过Http404对象抛出404“页面未找到”异常。

在下一章我们将继续扩展我们的应用程序,我们将增加用户管理功能,比如用户的注册和用户的登录等,下一章将讲述更多Django的功能,跟在我继续学习吧。


Tuesday, December 02, 2008

Django入门

开始Django之旅


章节介绍


Django和Python可以在多个操作系统平台是使用,本章我们将讲述如何在UNIX/Linux, Windows 和 Mac OS X 上搭建Django开发环境,而且会创建第一个Django应用并把它链接到数据库上。

我们将学习下面这些内容:

  • 安装Python

  • 安装Django

  • 安装一个数据库系统

  • 创建第一个项目

  • 配置数据库环境

  • 运行这个应用程序

搭建开发环境

我们的开发环境包括了Django,Python和一个数据库,有很多数据库可供选择,但是我们下面的例子中使用了Sqlite3数据库,因为这个数据库直接包含在了Python的下载版本中,这一节中我们将学习如何安装这些必要的软件。

安装Python

Django是由Python开发的,所以我们搭建开发环境的第一步是安装Python,Django需要Python2.3或以上版本,建议最好安装Python的最新版本。Python可以运行在大多数操作系统平台上,而且安装也非常简单,由于操作系统平台的不同安装方法也各不相同,下面我们将分别介绍如何在不同的主流操作系统平台上安装Python:.

在Windows上安装Python

Windows上安装Python非常简单,你可以从Python站点http://www.python.org/download/ 上下载一个标准的windows安装包。然后像安装其他windows软件一样,双击.exe文件,然后按照图形化的提示一步步安装完成即可。之后需要做的就是设置环境变量,这样你就可以通过命令行的方式使用Python了。方法是打开“控制面板”,双击“系统”,然后选择“高级”,单击“环境变量”按钮,找到“系统环境变量”中的path,把刚才Python的安装路径加在后面就可以了,记住新增的环境变量要用分号隔开。

好了这样windows上的Python就安装好了,如果你想试试安装是否成功,可以打开一个 命令行,然后输入python,如果你看到一个shell交换环境,恭喜你,Python已经安装成功了。

在UNIX/Linux上安装Python

如果你使用的是UNIX/Linux平台,那么你应该不用再单独安装Python了,因为它们是操作系统内置安装的。为了检查系统上是否安装了Python,打开一个终端,然后输入python并回车,你应该看到类似下面的信息,这说明系统上已经安装了Python:

Python 2.5.1 (r251:54863, May 2 2007, 16:56:35)

[GCC 4.1.2 (Ubuntu 4.1.2-0ubuntu4)] on linux2

Type "help", "copyright", "credits" or "license" for more information.

>>>

上面信息的第一行说明了你当前使用的版本,如果你得到的是错误或者是一个低版本的Python,那么你就需要安装最新版本的Python了。Unix和Linux用户建议通过系统包管理器更新最新的Python,对于其他APT的Linux(比如Debian 和Ubuntu),安装方法是这样的:打开一个终端输入如下内容:

$ sudo apt-get update

$ sudo apt-get install python

然后按照提示安装,其他类型的Linux用户请参照当前使用Linux的说明手册进行安装。

在Mac OS上安装Python

Mac OS上一般已经内置安装了Python,如果你的Python版本低于2.3,请从http://www.python.org/download/mac/ 下载最新的Python安装包进行安装。好了,现在Python的安装我们已经介绍完了,下面介绍如何安装Djiango。

安装Django

Django的安装非常简单,只是不同的操作系统略有不同。由于Python可以运行于任何操作系统上,所以Django也提供了一个可以运行于不同操作系统单独的包。我们可以从http://www.djangoproject.com/download/ 上下载最新的Django安装包,本书的例子是基于Django0.96的,不过大部分代码应该同样可以运行在最新的Django下。

在Windows上安装Django

下载最新的Django,将其解压缩到c:\下,打开一个命令行并进入Django的解压缩目录中。比如c:\>cd c:\Django-x.xx(x是Django的版本号),然后执行下面的命令来安装Django。

c:\Django-x.xx>python setup.py install

如果上面的方法行不通,你可以把解压缩的Django直接复制到python的包路径下Lib\site-packages。最后一步是把django-admin.py文件(位于Django-x.xx\django\bin)复制到系统路径下比如c:\windows\或者Python的安装路径下,比如c:\python25。执行完以上步骤后,你就可以把原来解压缩的Django安装包删除了,因为我们已经不在需要他了。好了,下面我们来测试一下,Django是否已经被正确的安装了:

c:\>django-admin.py --version

如果你看到了Django相关的安装信息,那么说明你已经成功的安装了Django。

在Unix/Linux和Mac OS上安装Django

在这些平台上安装Django是非常简单的,只要将Django安装包解压缩到一个目录下,执行相应命令就可以了:

$ tar xfz Django-x.xx.tar.gz

$ cd Django-x.xx

$ sudo python setup.py install

如果你的Linux是Ubuntu,你可以直接通过包管理控制台安装一个名为django-python的包。或者执行下面的命令:

$ sudo apt-get install python-django

然后通过下面的命令看看安装是否成功:

$ django-admin.py --version

如果你看到Django的版本信息,恭喜你Django已经安装成功了。

安装数据库系统

运行Django是不需要安装Django的,但是对于我们应用程序的需求,我们想把我们的业务数据存储在数据库中,现在看看如何在Django中安装数据库。

值得一提的是Django支持很多数据库引擎:MySQL, PostgreSQL, MS SQL Server, Oracle, 以及 SQLite。然后庆幸的是,你只需要掌握一种单独的数据库API就可以了,这归功于Django的数据层将不同的数据库访问接口进行了很好的抽象。我们在后面会讲到这部分内容,不过现在你只要知道,在Django中不同数据库之间的切换是非常简单的。


如果你安装的是Python2.5或者以上的版本,那么在开发阶段你可以不用安装数据库,Python安装中已经自带了sqlite3的模块。和通常的客户端/服务器模式的数据库引擎不同,sqlite把数据存储在一个单独的文件中。因此本书中的例子将使用SQLite作为我们的数据库引擎。当然你可以安装其他数据库引擎,我们只要简单的修改一个配置文件就可以了。比如你想使用Mysql,除了在系统上安装最新的Mysql发现版本外,你需要下载一个Python的Mysql数据库驱动程序包,然后修改Django中的数据库配置文件就可以了。

如果你安装的不是Python2.5那么从http://www.pysqlite.org/ 可以下载到SQLite的Python安装包。

我们需要安装Apache 或者其他web应用服务器吗?答案是,在开发阶段这是不必要的。Django提供了一个内置的轻量级的web服务器,当我们的程序要部署到真正的产品环境中时,我们在把它放到Apache上,后面的章节会讲到如何将部署。不过就像Django中数据库之间的切换非常方便一样,将你的应用程序发布的Apache这样的web服务器上也是非常简单的。

创建第一个项目

现在所有必需的软件都已经装好了,是时候开始我们的Django之旅了,我们先来创建第一个项目。

你一定还记得在我们前面的小节中介绍如何校验Django是否安装成功的时候用到了一个文件django-admin.py,它是Django的核心工具,你可以通过它完成一些列的有关项目创建和管理控制的任务,包括:

  • 创建一个新的项目

  • 创建并管理项目的数据库

  • 对当前的项目进行校验

  • 启动web服务器

我们将在后面的内容中通过一个“书签共享”的应用程序来逐一介绍如何完成这些任务。

创建一个空的项目

要创建一个空的项目我们需要打开终端(Windows用户打开一个命令行控制台)并输入下面的内容:

$ django-admin.py startproject django_bookmarks

这个命令将创建一个名为django_bookmarks的文件夹并在下面创建一些必要的项目初始化文件,让我们看看都是些什么文件:

django_bookmarks/

         __init__.py

manage.py

settings.py

urls.py

让我来解释一下这些文件都是做什么用的:


文件名

解释

__init__.py

Django的项目(project)实际上是Python的一个(package),所以这个文件告诉Python,将这个项目当作来对待。

Python中的,是包含了一些列Python模块文件的文件夹,这种方式可以有效的避免名称的冲突

manage.py

这是另一个有用的工具脚本文件,你可以把它想象成基于你项目的django-adming.py文件,实际上这个两个文件在Django内部是共享了后台的代码。

settings.py

这是Django项目的主要配置文件,你可以在这里设置很多选项,比如指定数据库、设置站点语言、打开或关闭Django内建功能等等。我们会在后面的章节中注意介绍这个文件的用处,不过本章我们只谈谈如何通过这个文件来设置数据库。

url.py

这是Django项目的另一个配置文件,你可以把它看成是URL和Python函数之间的映射关系的定义。这个文件是Django中提供的强大功能之一,我们将在后面的章节中介绍如何使用这个文件。


我们在写程序的时候这个文件夹相当于我们程序的一个容器,所有代码都包含在这里。现在让我们来看看如何配置Django的数据库。

数据库配置

现在我们开始了真正的Django编码工作了。那么我们应该选一个代码编辑器,市面上有很多可选的代码编辑工具,有些人喜欢用功能完善的集成开发工具,有的人喜欢喜用简单快速的文本编辑器,如果你现在已经有一个习惯使用的代码编辑器,我建议你继续使用下去,如果还没有这里有几个不错的工具:

  • Scite:这是一个功能强大的编辑器,它是开源软件可以运行在任何的主流平台上,支持语法高亮和自动补全功能。你可以在http://www.scintilla.org/SciTE.html 下载到。
  • EditPlus:这是另一个功能强大的编辑器,它是用于Windows平台,你可以通过安装插件的方式来提供Python语法的支持。http://www.editplus.com/ 可以下载到。
  • TextMate:这是一个在MacOS上广泛使用的文本编辑器,它提供了丰富的对Django的支持。TextMate不是免费的,但是你可以在这个http://macromates.com/下载到30天的试用版本。
  • Eclipse + PyDev:这是Eclipse集成开发环境的Python版本,它提供了语法高亮,自动补全已经版本控制等一系列功能。如果你选择使用IDE来开发Django,向你推荐这个功能。有关下载和安装PyDev的信息可以在http://pydev.sourceforge.net/ 找到。

现在编辑器已经准备好了,让我们贷款settings.py文件看看里边包含些什么内容:

# Django settings for django_bookmarks project.

DEBUG = True
TEMPLATE_DEBUG = DEBUG

ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)

MANAGERS = ADMINS

DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = '' # Or path to database file if using sqlite3.
DATABASE_USER = '' # Not used with sqlite3.
DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.

。。。。。

你可能已经注意到了,这个文件中包含了一系列的变量,没个变量都有相应的解释,说明这个变量的含义和用处。

我们现在关注的是如何配置数据库,就像我们前面提到的那样Django支持很多数据库引擎,所以首先我们要告诉Django我们要使用那种数据库。这由DATABASE_ENGINE变量来控制,刚才说过我们准备使用SQLite,所以把这个变量的值设置为“sqlite3”。然后就是定义数据库的名称。将DATABASE_NAME变量赋值为“bookmarksdb”。我们现在使用的是SQLite数据库所以这些就足够了。如果使用的是其他的数据库服务器,那么你还需要定义诸如:数据库用户名、口令、数据库服务器地址和端口号。通过以上设置,这部分看起来应该是这样:

DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = 'bookmarksdb'
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

接下来要做的事就是告诉Django,我们的数据库已经设置好了现在可以创建数据库和必要的表。尽管现在我们还没有定义任何表,但是Django的一些内建功能需要在数据库中创建一些附加表。创建这些表的过程非常简单,只要执行下面的命令就可以了:

$ python manage.py syncdb

执行上面的命令后,你应该看到一些列输出消息,你可以看到Django自动创建了一些表,并且询问你是否创建一个超级用户。我们应该创建一个超级用于以便可以访问我的刚刚创建的应用程序。如果一切正常我们现在可以启动我们的web服务器了。

启动web服务器

我们前面曾经说过,Django内置了一个开发用的web服务器,这是一个轻量级的服务器,专门为Django而预置。重要的一点是,每次你更改Django代码的时候这个服务器都会自动重新启动。启动服务器的方法很简单,只要运行下面的命令就可以了。

$ python manage.py runserver

然后打开你的浏览器输入地址http://127.0.0.1:8000/你将看到下面的页面:


恭喜你,第一个Django应用程序已经完成了,在接下来的一章中我们将基于这个空的应用程序逐步丰富他的内容,同时进一步介绍Django。

本章总结

在这一章我们学习了如何搭建开发运行环境,并创建了第一个应用程序,学习了如何运行Django的web服务器,现在所以的准备工作都做好了。

下面是一个本章中我们学到的Django功能的总结:

  • 你可以从http://www.djangoproject.com/下载Django,它是由Python写成的,可以运行于所有主流的操作系统。
  • 通过下面的命令创建一个显得Django应用程序,
$ django-admin.py startproject <project-name>
  • 通过下面的命令创建数据库表
$ python manage.py syncdb
  • 通过下面的命令启动web服务器
$ python manage.py runserver
  • Django应用程序的配置文件是settings.py,它是一个Python源文件,大部分Django的配置内容都在这里完成。

下一章我们将逐步丰富这个应用程序,并介绍Django中主要的组件和功能。