============================
Django Step by Step (六)
============================

:作者: limodou
:联系: limodou@gmail.com
:版本: 0.1
:主页: http://wiki.woodpecker.org.cn/moin/NewEdit
:BLOG: http://www.donews.net/limodou
:版权: FDL

.. contents:: 目录
.. sectnum::

引言
=====

以后的例子可能会越来越复杂，没办法因为我们用的东西越来越复杂，同时我们的能力也在增长。

下面我们按照 TurboGears_ 的 `20 Minute Wiki Tutorial`_ 的例子仿照一个，我们要用 Django_ 来做 wiki。我不会按 TurboGears 的操作去做，只是实现一个我认为的最简单的 wiki。

.. _TurboGears: http://www.turbogears.org/
.. _`20 Minute Wiki Tutorial`: http://www.turbogears.org/docs/wiki20/
.. _Django: http://www.djangoproject.com/

现在我的要求是：

    做一个简单的wiki，要可以修改当前页面，即在页面下面提供一个编辑的按钮。然后还要识别页面中的两个开头大写的单词为页面切换点，可以进入一个已经生成好的页面，或提示创建一个新页面。

下面我们将开始创建 Django 中的 app 了。

先说一下。如果你看过官方版的教程，它就是讲述了一个 Poll 的 app 的生成过程。那么一个 app 就是一个功能的集合，它有自已的 model ，view 和相应的模板，还可以带自已的 urls.py 。那么它也是一个独立的目录，这样一个 app 就可以独立地进行安装，你可以把它安装到其它的 Django 服务器中去。因此采用 app 的组织形式非常有意义。而且 ``adango-admin.py`` 也提供了一个针对 app 的命令，一会我们就会看到。而且 Django 提供一些自动功能也完全是针对于 app 这种结构的。Model, Template, View 就合成了 MTV 这几个字母。 Model 是用来针对数据库，同时它可以用来自动生成管理界面， View 在前面我们一直都用它，用来处理请求和响应的相当于MVC框架中的 Controller 的作用， Template 用来生成界面。

创建 wiki app
==============

::

    manage.py startapp wiki
    
.. note:: 在 0.92 版之前， app 都是放在 apps 目录下的。不过到了 0.92 版，apps 目录不自动创建了。因此你就可以直接放在项目目录下了。
    
这样在 ``wiki`` 子目录下有以下文件:

    ``__init__.py``
        表示 ``wiki`` 目录是一个包。
        
    ``views.py``
        用来放它的 view 的代码。
        
    ``models.py``
        用来放 model 代码。

编辑 wiki/models.py
==========================

::

    from django.db import models
    
    # Create your models here.
    class Wiki(models.Model):
        pagename = models.CharField(maxlength=20, unique=True)
        content = models.TextField()

每个 model 其实在 Django 中就是一个表，你将用它来保存数据。在实际的应用中，一般都要与数据库打交道，如果你不想用数据库，那么原因可能就是操作数据库麻烦，创建数据库环境也麻烦。但通过 Django 的 model 处理，它是一种 ORM (Object Relation Mapping, 对象与关系的映射)，可以屏蔽掉底层数据库的细节，同时提供以对象的形式来处理数据。非常方便。而且 Django 的 model 层支持多种数据库，如果你改变数据库也不是什么问题，这也为以后的数据库迁移带来好处。总之，好处多多，大家多多体会吧。

``Wiki`` 是 model 的名字，它需要从 ``models.Model`` 派生而来。它定义了两个字段，一个是字段是 ``pagename`` ， 用来保存 wiki 页面的名字，它有两个参数，一个是最大长度(不过从这点上不如 SQLAlchemy_ 方便, SQLAlchemy并不需要长度，它会根据有无长度自动转为 ``TEXT`` 类型)，目前 ``CharField`` 需要这个参数；另一个是 ``unique`` 表示这个字段不能有重复值。还有一个字段是 ``content`` ，用来保存 wiki 页面的内容，它是一个 ``TextField`` 类型，它不需要最大长度。

.. _SQLAlchemy: http://www.sqlalchemy.org/

.. note:: models.Model 在 0.92 版以前是 meta.Model 。而 django.db 在 0.92 版之前是 django.core 。

现在不太了解 model 没有关系，关键是看整个生成过程。

一旦你定义好了 model ，在运行时， Django 会自动地为这个 model 增加许多数据操作的方法。关于 model 和 数据库操作API的详细内容参见 `Model reference`_ 和 `Database API reference`_ 的文档。

.. _`Model reference`: http://www.djangoproject.com/documentation/model_api/
.. _`Database API reference`: http://www.djangoproject.com/documentation/db_api/

修改 settings.py, 安装 app
===========================

虽然我们的其它工作没有做完，但我还是想先安装一下 app 吧。每个一 app 都需要安装一下。安装一般有两步：

a) 修改settings.py

::

    INSTALLED_APPS = (
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.sites',
        'newtest.wiki',
    )

这个在文件的最后，前4个是缺省定义的。给出指定 wiki 包的引用名来。这一步是为了以后方便地导入所必须的。因为我们的目录都是包的形式，因此这里就是与目录相对应的。

b) 执行(在newtest目录下)

::

    manage.py syncdb
    
.. note:: 以前是使用 install wiki 。现在也可以使用，不过使用 syncdb 要更简单得多。

如果没有报错就是成功了。这一步 Django 将根据 model 的信息在数据库中创建相应的表。表就是这样创建出来的。

在命令行下加入首页(FrontPage)
===============================

我们假设首页的名字为 FrontPage ，并且我们将在命令行下增加它，让我们熟悉一下命令行的使用

进入 ``newtest`` 目录，然后::

    manage.py shell 
    

.. _NewEdit: http://wiki.woodpecker.org.cn/moin/NewEdit

进入 python

::

    >>> from newtest.wiki.models import Wiki
    >>> page = Wiki(pagename='FrontPage', content='Welcome to Easy Wiki')
    >>> page.save()
    >>> Wiki.objects.all()
    [<Wiki object>]
    >>> p = Wiki.objects.all()[0]
    >>> p.pagename
    'FrontPage'
    >>> p.content
    'Welcome to Easy Wiki'

.. note:: 因为在写这篇教程时是在magic-removal分枝下进行的操作，因此有些 API 并不稳定。象 ``objects`` 的方法以前是沿用model的方法，但后来进行了简化，比如 ``get_list()`` 变为 ``all()`` 。还有一系统的变化。具体的可以参见 `Removing The Magic`_ 文档中关于 Descriptor fields 的说明。

.. _`Removing The Magic`: http://code.djangoproject.com/wiki/RemovingTheMagic

在 Django 中，对于数据库的记录有两种操纵方式，一种是集合方式，一种是对象方式。集合方式相当于表级操作，在新版的 0.92 中可以使用 ``model.objects`` 来处理。 objects 对象有一些集合方式的操作，如 ``all()`` 会返回全部记录， ``filter()`` 会根据条件返回部分记录。而象插入新记录则需要使用记录方式来操作，些时要直接使用 model 类。

.. note:: 在 0.92 版之前要比这麻烦得多，也不容易理解。好在情况已经发生了变化。

修改 wiki/views.py
=========================

::

    #coding=utf-8
    from newtest.wiki.models import Wiki
    from django.template import loader, Context
    from django.http import HttpResponse, HttpResponseRedirect
    from django.shortcuts import render_to_response
    
    def index(request, pagename=""):
        """显示正常页面，对页面的文字做特殊的链接处理"""
        if pagename:
            #查找是否已经存在页面
    #        pages = Wiki.objects.get_list(pagename__exact=pagename)
            pages = Wiki.objects.filter(pagename=pagename)
            if pages:
                #存在则调用页面模板进行显示
                return process('wiki/page.html', pages[0])
            else:
                #不存在则进入编辑画面
                return render_to_response('wiki/edit.html', {'pagename':pagename})
            
        else:
    #        page = Wiki.objects.get_object(pagename__exact='FrontPage')
            page = Wiki.objects.get(pagename='FrontPage')
            return process('wiki/page.html', page)
        
    def edit(request, pagename):
        """显示编辑存在页面"""
    #    page = Wiki.objects.get_object(pagename__exact=pagename)
        page = Wiki.objects.get(pagename=pagename)
        return render_to_response('wiki/edit.html', {'pagename':pagename, 'content':page.content})
    
    def save(request, pagename):
        """保存页面内容，老页面进行内容替换，新页面生成新记录"""
        content = request.POST['content']
    #    pages = Wiki.objects.get_list(pagename__exact=pagename)
        pages = Wiki.objects.filter(pagename=pagename)
        if pages:
            pages[0].content = content
            pages[0].save()
        else:
            page = Wiki(pagename=pagename, content=content)
            page.save()
        return HttpResponseRedirect("/wiki/%s" % pagename)
    
    import re
    
    r = re.compile(r'\b(([A-Z]+[a-z]+){2,})\b')
    def process(template, page):
        """处理页面链接，并且将回车符转为<br>"""
        t = loader.get_template(template)
        content = r.sub(r'<a href="/wiki/\1">\1</a>', page.content)
        content = re.sub(r'[\n\r]+', '<br>', content)
        c = Context({'pagename':page.pagename, 'content':content})
        return HttpResponse(t.render(c))
            
.. note:: 将原来老的 model 方法加了注释，目前改用最新的 API 了。

代码有些长，有些地方已经有说明和注释了。简单说一下：

* ``index()`` 用来显示一个 wiki 页面。它需要一个参数就是页面的名称。如果在数据库中找得到，则调用 ``process()`` 方法(``process()`` 方法是一个自定义方法，主要用来对页面的文本进行处理，比如查找是否有满足 wiki 命名规则的单词，如果有则替换成链接。再有就是将回车转为 ``<br>`` )。如果没有找到，则直接调用编辑模板显示一个编程页面。当然，这个页面的内容是空的。只是它的页面名字就是 ``pagename`` 。如果 ``pagename`` 为空，则进入 ``FrontPage`` 页面。 ``Wiki.objects`` 对象有 ``filter()`` 方法和 ``get()`` 方法，一个返回一个结果集，一个返回指定的对象。这里为什么使用 ``filter()`` 呢，因为一旦指定文件不存在，它并不是返回一个 ``None`` 对象，而是抛出异常，而我没有使用异常的处理方式。通过 ``filter()`` 如果存在则结果中应有一个元素，如果不存在则应该是一个 ``[]`` 。这样就知道是否有返回了。
    
.. note:: ``filter()`` 中使用的参数与一般的 db-api 是一样的，但如果是比较相等，可以为: ``pagename__exact=pagename`` 也可以简化为 ``pagename=pagename`` 。

.. note:: 在 Django 中，一些字段的比较操作比较特殊，它是在字段名后加 ``__`` 然后是比较条件。这样看上去就是一个字符串。具体的参见 `The Database API`_ 。

.. _`The Database API`: http://www.djangoproject.com/documentation/db_api/

.. note:: 回车转换的工作其实可以在模板中使用 filter 来完成。

* 从对模板的使用 (wiki/edit.html) 可以猜到在后面我们要在 ``templates`` 中创建子目录了。的确，对于不同的 app ，我们可以考虑将所有的模板都放在统一的 ``templates`` 目录下，但为了区分方便，一般都会针对 app 创建不同的子目录。当然也可以不这样，可以放在其它的地方，只要修改 ``settings.py`` ，将新的模板目录加进去就行了。

因为我们在设计 model 时已经设置了 ``pagename`` 必须是唯一的，因此一旦 ``filter()`` 有返回值，那它只能有一个元素，而 ``pages[0]`` 就是我们想要的对象。

* ``page = wikis.get(pagename='FrontPage')``
    
  是表示取出 ``pagename`` 为 ``FrontPage`` 的页面。你可能要说，为什么没有异常保护，是的，这也就是为什么我们要在前面先要插条记录在里面的原因。这样就不会出错了。再加上我要做的 wiki 不提供删除功能，因此不用担心会出现异常。

* ``edit()`` 用来显示一个编辑页面，它直接取出一个页面对象，然后调用 wiki/edit.html 模板进行显示。也许你还是要问，为什么不考虑异常，因为这里不会出现。为什么？因为 ``edit()`` 只用在已经存在的页面上，它将用于存在页面的修改。而对于不存在的页面是在 ``index()`` 中直接调用模板来处理，并没有直接使用这个 ``edit()`` 来处理。也许你认为这样可能不好，但由于在 ``edit()`` 要重新检索数据库，而在 ``index()`` 已经检索过一次了，没有必要再次检索，因此象我这样处理也没什么不好，效率可能要高一些。当然这只是个人意见。

* ``save()`` 用来在编辑页面时用来保存内容的。它先检查页面是否在数据库中存在，如果不存在则创建一个新的对象，并且保存。注意，在 Django 中，对对象处理之后只有调用它的 ``save()`` 方法才可以真正保存到数据库中去。如果页面已经存在，则更新页面的内容。处理之后再重定向到 ``index()`` 去显示这个页面。

在 templates 中创建 wiki 子目录
================================

编辑 templates/wiki/page.html
================================

::

    <h2>{{ pagename }}</h2>
    <p>{{ content }}</p>
    <hr/>
    <p>
    <form method="POST" action="/wiki/{{ pagename }}/edit/">
    <input type="submit" value="编辑">
    </form></p>

它用来显示页面，同时提供一个“编辑”按钮。当点击这个按钮时将调用 view 中的 ``edit()`` 方法。

编辑 templates/wiki/edit.html
===============================

::

    <h2>编辑:{{ pagename }}</h2>
    <form method="POST" action="/wiki/{{pagename}}/save/">
    <textarea name="content" rows="10" cols="50">{{ content }}</textarea><br/>
    <input type="submit" value="保存">
    </form>

它用来显示一个编辑页面，同时提供“保存”按钮。点击了保存按钮之后，会调用 view 中的 save() 方法。

修改 urls.py
==============

::

    from django.conf.urls.defaults import *
    
    urlpatterns = patterns('',
        # Example:
        # (r'^testit/', include('newtest.apps.foo.urls.foo')),
        (r'^$', 'newtest.helloworld.index'),
        (r'^add/$', 'newtest.add.index'),
        (r'^list/$', 'newtest.list.index'),
        (r'^csv/(?P<filename>\w+)/$', 'newtest.csv_test.output'),
        (r'^login/$', 'newtest.login.login'),
        (r'^logout/$', 'newtest.login.logout'),
        (r'^wiki/$', 'newtest.wiki.views.index'),
        (r'^wiki/(?P<pagename>\w+)/$', 'newtest.wiki.views.index'),
        (r'^wiki/(?P<pagename>\w+)/edit/$', 'newtest.wiki.views.edit'),
        (r'^wiki/(?P<pagename>\w+)/save/$', 'newtest.wiki.views.save'),
    
        # Uncomment this for admin:
    #     (r'^admin/', include('django.contrib.admin.urls')),
    )

增加了 wiki 等4个 url 映射。

这里要好好讲一讲 URL 的设计(个人所见)。

一般一个 wiki ，我们访问它的一个页面可能为：wiki/pagename。因此我设计对 index() 方法的调用的 url 为::

    r'^wiki/(?P<pagename>\w+)/$'

也就是把 wiki/后面的解析出来作为 ``pagename`` 参数。但这样就带来一个问题，如果我想实现 ``wiki/edit.html`` 表示修改， ``pagename`` 作为一个参数通过 POST 来提交好象就不行了。因为上面的解析规则会“吃”掉这种情况。因此我采用 Zope_ 的表示方法：把对象的方法放在对象的后面。我可以把 ``pagename`` 看成为一个对象， ``edit`` ,  ``save`` 是它的方法，放在它的后面，也简单，也清晰。当然如果我们加强上面的正则表达式，也可以解析出 ``wiki/edit.html`` 的情况，但那就是你设计的问题了。这里就是我的设计。

.. _Zope: http://www.zope.org/

因此 wiki/pagename 就是显示一个页面，wiki/pagename/edit 就是编辑这个页面， wiki/pagename/save 就是保存页面。而 ``pagename`` 解析出来后就是分别与 ``index()`` ,  ``edit()`` ,  ``save()`` 的 ``pagename`` 参数相对应。

下面你可以运行了。

启动 server
=============

进入 http://localhost:8000/wiki

首先进入这个页面：

.. image:: tut06_01.jpg

然后你点编辑，则进入FrontPage的编辑界面：

.. image:: tut06_02.jpg

然后我们加上一个 TestPage ，它符合 wiki 的名字要求，两个首字母大写的单词连在一起。然后点击保存。

.. image:: tut06_03.jpg

看见了吧。页面上的 TestPage 有了链接。点击它将进入：

.. image:: tut06_04.jpg

这是 TestPage 的编辑页面。让我们输入中文，然后输入 FrontPage 。然后保存。

.. image:: tut06_05.jpg

好了，剩下的你来玩吧。点击 FrontPage 将回到首页。
