2005年12月31日

让我们再仔细看一下这个通讯录,我们知道,如果想增加新的记录,一种方法是通过 admin 界面,这个已经由 django 自动为我们做好了。我们还可以批量导入,这个是我们实现的。但是这里有风险,为什么,如果什么人都可以导入这可是件不好的事,那么怎么办:加权限控制。

django 自带了一个权限控制系统,那么我们就用它。因此先让我们简单地了解一下 django 中的权限。同时我希望只有特殊权限的人才可以做这件事情,在前几讲中我们一直使用 admin (超级用户)这个用户,但这并不是个好的习惯。因此让我们先创建个个人用户吧。

1. 添加一个个人用户

使用 admin 用户进入管理界面 http://localhost:8000/admin

在 Auth 下有用户一项,点击添加按钮进入添加界面,还挺复杂的。在这里提示是黑体的字段是必输项,其实只有两项是需要我们输的:用户名和口令。用户名好办,口令怎么还有格式呢?

格式为:’[algo]$[salt]$[hexdigest]‘
这里 algo 是算法的名字,可以是 md5 或 sha1 算法。salt 是一个随机数,它将用来参与密码信息的生成。hexdigest是将 salt+原始的密码 计算它的摘要算法得出来的东西。从 authentication 文档来看,并没有仔细地解释这个事情。我们需要这样做吗?但实际的情况要好,也要复杂的多。

我们其实并不一定需要这样做,django 在做口令检查时,一旦发现口令串不是组织成以’$'分隔的形式,它会先认为是md5算出的结果,然后如果比较成功则自动使用sha1重新计算,然后保存到数据库中去。因此,最简单的就是按文档上那样,使用 Python 来生成一个md5的口令摘要码,如:

>>> import md5
>>> md5.new(‘test’).hexdigest()
‘098f6bcd4621d373cade4e832627b4f6′

上面就生成了一个口令为’test’的md5的摘要码。然后把它拷贝到输入口令的地方即可。然后在我们第一次成功验录后,django 会自动替我们改成三段的格式,而我们不需要知道。不过,对于一般户其实不用提心,因为他们没有机会创建自已的用户,这一切都是管理员的工作,一般用户只是在管理员设定好口令之后,他们登录,然后可以修改自已的口令。所以真正麻烦的是管理员。我不知道 django 为什么会这样,只是看到邮件列表中的确有人在讨论这个问题,以后再关注吧。

知道了口令应该如何生成(md5计算),那么我们只要填入用户名,口令就行了。但是还有其它的一些选项要注意:

人员状态检查框如果不打勾,则你的用户也无法使用,因为他不能登录。也许你提心,如果打勾了,那不是能做好多事。其实不然,只要不是超级用户,又没有给他赋上面许可中的任何权限,他什么也做不了,只是登录,但这也许就够了,有时我们需要的就是一个用户的合法身份,而不是一定要他能做些什么。

一旦有了用户,在 request 对象中提供一个 user 对象,你可以根据它来判断当前用户的身份,所属的组,所拥有的权限。我们可以在 view 代码中进行用户身份的检查。

现在我的想法是:限制固定用户来做这件事。首先我可以在 settings.py 中设定这个用户名,然后在 view 中检查当前用户是否是 settings.py 中设定的用户。

2. 修改 settings.py

在最后增加:

UPLOAD_USER = ‘limodou’

这里请把 limodou 改成你的名字。要注意,在后面的测试中你需要按这里指定的名字创建一个用户。

3. 修改 apps/address/views.py

#…
from django.conf.settings import UPLOAD_USER

def upload(request):
    if request.user.username != UPLOAD_USER:
        return render_to_response(‘address/error’, {’message’:'你需要使用 %s 来登录!’ % UPLOAD_USER})
#…

我们从 django.conf.settings 中导出了 UPLOAD_USER,然后在 upload() 中判断当前用户名是否是这个用户名,如果不是则提示出错信息。否则继续处理。

好象一切都挺简单,但这里还有一个大问题:能不能自动导到用户注册的画面呢?上面的处理是需要用户进入 /admin/ 进行注册后,再进行操作。如果没有注册就上传文件,则只会报错。这里我希望如果用户没有注册过,自动显示一个注册画面。如何做呢?

文档中提出了一个方法:

from django.views.decorators.auth import login_required

@login_required
def my_view(request):
    # …

这个方法我试过了,但失败了。主要的原因是:如果你还没有注册,它会自动导向 /accounts/login/ ,而这个URL目前是不存在的。在我分析了 login.py 代码之后,我认为它只是一个框架,并不存在 django 已经提供好的模板可以直接使用,如果要使用它是不是需要我自已去建一个?没办法,我分析了 admin 的代码,最终找到了一种替代的方法:

from django.contrib.admin.views.decorators import staff_member_required

@staff_member_required
def upload(request):

admin 已经提供了这样的一个方法:staff_member_required。它允许我使用 admin 的登录画面。

一旦把上面的代码补充完整,代码是这样的:

#coding=utf-8
# Create your views here.
from django.models.address import addresses
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.core.extensions import render_to_response
from django.core.template import loader, Context
from django.contrib.admin.views.decorators import staff_member_required
from django.conf.settings import UPLOAD_USER

@staff_member_required
def upload(request):
    if request.user.username != UPLOAD_USER:
        return render_to_response(‘address/error’, {’message’:'你需要使用 %s 来登录!’ % UPLOAD_USER})
    file_obj = request.FILES.get(‘file’, None)
    if file_obj:
        import csv
        import StringIO
        buf = StringIO.StringIO(file_obj['content'])
        try:
            reader = csv.reader(buf)
        except:
            return render_to_response(‘address/error’, {’message’:'你需要上传一个csv格式的文件!’})
        for row in reader:
            objs = addresses.get_list(name__exact=row[0])
            if not objs:
                obj = addresses.Address(name=row[0], gender=row[1], telphone=row[2], mobile=row[3], room=row[4])
            else:
                obj = objs[0]
                obj.gender = row[1]
                obj.telphone = row[2]
                obj.mobile = row[3]
                obj.room = row[4]
            obj.save()
           
        return HttpResponseRedirect(‘/address/’)
    else:
        return render_to_response(‘address/error’, {’message’:'你需要上传一个文件!’})
   
def output(request):
    response = HttpResponse(mimetype=’text/csv’)
    response['Content-Disposition'] = ‘attachment; filename=%s’ % ‘address.csv’
    t = loader.get_template(‘csv’)
    objs = addresses.get_list()
    d = []
    for o in objs:
        d.append((o.name, o.gender, o.telphone, o.mobile, o.room))
    c = Context({
        ‘data’: d,
    })
    response.write(t.render(c))
    return response

基本没有变化,主要是开始的一些地方。

4. 启动 server 测试一下吧。

在点击上传之后,如果没有注册会进入登录画面。如果已经注册,但用户名不对,则提示一个出错信息。不过,一旦注册出错,没有提供自动重新登录的功能,因此你需要进入 /admin/ 地址,然后注销当前用户,再重新上传或先用正确的用户登录。因为是个简单的 app ,没必要做得那么完善。同时还存在的一个问题是,如果你没有注册过,那么点击上传按钮后,将进入登录画面,但如果成功,你上传的文件将失效,需要重新再上传。那么解决这个问题的一个好方法就是:不要直接显示上传的东西,而是先提供一个链接或按钮,认证通过后,再提供上传的页面,这样可能更好一些。

在 authentication 文档中还有许多的内容,如权限,在模板中如何使用与认证相关的变量,用户消息等内容。

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

首先让我们说一说 media 链接吧。在上一讲中我使用了 site_media 作为静态文件的起始目录。但你知道吗,原来我想使用的是 media ,但为什么又改了呢?原因就是:admin给占了。如果我使用 media ,django 会指向 admin 的media目录,这可不是你想要的。因此这一点要特别提醒。

现在我们看一看所展示出来的页面,你满意吗?还有可以改进的地方。比如性别,它显示出来的直接是数据库的值,而不是对应的“男”,“女”,怎么办。还有表格显示也不是很好看。没说的,改!

最初我想使用 CustomManipulator (Manipulator是 django 中用来自动生成元素对应的 HTML 代码的对象,你可以定制它),但使用 Manipulator 的话,你不能再使用 generic_view 了,需要自已去实现 generic_view 的某些代码,当然可以 copy and paste,但我目前不想那样做。于是我想到可以扩展 django 的模板,自定义一个 filter 来实现它。(具体扩展的文档参见 template_python ,你不仅可以扩展filter,还可以扩展tag,还可以设置模板变量,还可以进行块处理等复杂的操作,自已去看吧。)

1. 创建 apps/address/templatetags 目录

注意,这个目录要在某个应用的下面,同时它应与 models目录, views.py在同一层目录下。

2. 创建 apps/address/templatetags/__init__.py 文件

文件为空即可

3. 创建一个 Python 文件

文件名为你想要装入到模板中的名字。如文件起名为 change_gender.py ,那么你将可以在模板中使用

{% load change_gender %}

来导入。

4. 编辑 change_gender.py

#coding=utf-8
from django.core import template

register = template.Library()

#@register.filter(name=’change_gender’)
def change_gender(value):
    if value == ‘M’:
        return ‘男’
    else:
        return ‘女’
   
register.filter(‘change_gender’, change_gender)

先是导入 template 模块,然后生成一个 register 的对象,我将用来它注册我所定义的 filter 。我实现的 filter 将命名为 "change_gender",它没有参数(一个filter可以接受一个参数,或没有参数)。当value为’M'时返回“男”,当value为’F'时返回“女”。然后调用register的filter来注册它。这里有两种写法,一种是使用python 2.4才支持的decorator(此行注释掉了),另一种是使用标准的写法。在使用decorator时,如果filter方法有多个参数的话,需要指明name参数,否则可以直接写为:

@register.filter

它自动将函数名认为是filter的名字。

5. 修改 address_list.html

{% extends "base" %}
{% block content %}
{% load change_gender %}
<style type="text/css">
h1#title {color:white;}
.mytr1 {background:#D9F9D0}
.mytr2 {background:#C1F8BA}
.myth {background:#003333}
.th_text {color:#ffffff}
</style>
<div id="header">
<h1 id="title">通讯录</h1>
</div>
<hr>
<div id="content-main">
    <table border="0" width="500">
    <tr align="right"><td>{% if has_previous %}<a href="/address?page={{ previous }}">上一页</a>{% endif %} {% if has_next %}<a href="/address?page={{ next }}">下一页</a>{% endif %}</td></tr>
    </table>
    <table border="0" width="500" cellspacing="2">
    <tr class="myth">
      <th><span class="th_text">姓名</span></th>
      <th><span class="th_text">性别</span></th>
      <th><span class="th_text">电话</span></th>
      <th><span class="th_text">手机</span></th>
      <th><span class="th_text">房间</span></th>
    </tr>
    {% for person in object_list %}
    <tr class="{% cycle mytr1,mytr2 %}"><td>{{ person.name }}</td><td>{{ person.gender|change_gender }}</td><td>{{ person.telphone }}</td><td>{{ person.mobile }}</td><td>{{ person.room }}</td></tr>
    {% endfor %}
    </table>
    <table border="0" width="500">
    <tr><td><form enctype="multipart/form-data" method="POST" action="/address/upload/">
    文件导入:<input type="file" name="file"/><br/>
    <input type="submit" value="上传文件"/>
    </form>
    </td>
    <td><p><a href="/address/output/">导出为csv文件</a></p></td></tr>
    </table>
</div>
{% endblock %}

同时美化了一下表格的CSS,使用了 cycle tag 来处理两个表格行的样式切换。cycle 处理的是字符串。

6. 启动 server

注意,一定要重启。象 templatetags 之类是在导入时处理的,因此如果 server 已经启动再添加的话是不起作用的。其它象增加 app, 修改 settings.py 都是要重启,而修改 urls.py, view, model代码,模板什么的可以不用重启,在必要时 django 的测试 web server 会自动重启。如果你使用 Apache 的话,估计绝大多数情况下要重启,可能也就修改模板不用吧。

如果一些成功,你会看到’M',’F'都改过来了。这里如果你感兴趣还可以改成小图标来表示,点缀一下。

效果画面为:

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

不知道大家有没有对这个通讯录感到厌烦了,希望没有,因为还有一些东西没有讲完呢。

最让我感觉不满意的就是通讯录的显示了,的确很难看,希望可以美化一下。那么主要从这几方面:

  • 对姓名进行排序
  • 生成分页结果
  • 增加css和一些图片

1. 修改 apps/address/models/address.py 实现排序

在META中增加一个 ordering = ['name'] 的属性即可。它表示按name进行排序。它可以有多个字段。如果在字段前加’-'表示倒序。修改完毕在浏览器中看一下效果就知道了。

2. 修改 templates/address/address_list.html 实现分页显示

<h2>通讯录</h2>
<hr>
<div>
<table border="0" width="500">
<tr align="right"><td>{% if has_previous %}<a href="/address?page={{ previous }}">上一页</a>{% endif %} {% if has_next %}<a href="/address?page={{ next }}">下一页</a>{% endif %}</td></tr>
</table>
<table border="1" width="500">
<tr><th>姓名</th><th>性别</th><th>电话</th><th>手机</th><th>房间</th></tr>
{% for person in object_list %}
<tr><td>{{ person.name }}</td><td>{{ person.gender }}</td><td>{{ person.telphone }}</td><td>{{ person.mobile }}</td><td>{{ person.room }}</td></tr>
{% endfor %}
</table>
</div>
<table border="0" width="500">
<tr><td><form enctype="multipart/form-data" method="POST" action="/address/upload/">
文件导入:<input type="file" name="file"/><br/>
<input type="submit" value="上传文件"/>
</form>
</td>
<td><p><a href="/address/output/">导出为csv文件</a></p></td></tr>
</table>

这时我仍然使用的是 generic_view 来处理。但对布局作了简单的调整,将导入和导出的内容移到下面去了。同时增加了对分页的支持。

{% if has_previous %}<a href="/address?page={{ previous }}">上一页</a>{% endif %} {% if has_next %}<a href="/address?page={{ next }}">下一页</a>{% endif %}

在使用generic_view的object_list时,它会根据 url dispatch 中是否设置了 paginate_by 这个参数来决定是否使用分页机制。一会我们会看到在 urls.py 的这个参数。一旦设置了这个参数,则 object_list 会使用 django 提供的一个分页处理器来实现分页。它会自动产生分页所用到的许多的变量,这里我们使用了 has_previous, previous, has_next, next这四个变量,还有其它一些变量可以使用。具体的参见 generic_views 的文档。

这里是根据是否有前一页和下一页来分别生成相应的链接。对于分页的链接,需要在url中增加一个query关键字page。因此我的模板中会生成page={{ previous }}和page={{ next }}分别指向前一页和下一页。

3. 修改 apps/address/urls.py

from django.conf.urls.defaults import *

info_dict = {
    ‘app_label’: ‘address’,
    ‘module_name’: ‘addresses’,

urlpatterns = patterns(”,
    (r’^/?$’, ‘django.views.generic.list_detail.object_list’, dict(paginate_by=10, **info_dict)),
    (r’^upload/$’, ‘newtest.apps.address.views.upload’),
    (r’^output/$’, ‘newtest.apps.address.views.output’),
)

修改了原来传给的object_list的参数,这里设置每页的条数为10条。

dict(paginate_by=10, **info_dict)

这是将新的参数与原来的参数生成一个新的字典。

4. 启动 server 测试一下

显示效果为

下面让我们为它添加一些CSS和图片,让它变得好看一些。

首先要说明一下,我们一直处于开发和测试阶段,因此我们一直使用的都是 django 自带的 server(其实我个人感觉这个server的速度也挺快的),但最终我们的目的是把它部署到 apache 上去。现在我们打算增加 CSS和添加一些图片,django 提供了这个能力,这就是对静态文件的支持,但是它只是建议在开发过程中使用。真正到了实际环境下,还是让专门的 web server,如Apache来做这些事情。只要改一下链接设置就好了。更详细的说明要参见 static_files 的文档。同时在 django 中为了不让你依赖这个功能,特别在文档的开始有强烈的声明:使用这个方法是低效和不安全的。同时当DEBUG设置(在settings.py中有这个选项,True表示处于调试期,会有一些特殊的功能)为False时,这个功能就自动无效了,除非你修改代码让它生效。

5. 修改 urls.py

from django.conf.urls.defaults import *
from django.conf.settings import STATIC_PATH

urlpatterns = patterns(”,
    # Example:
#    (r’^helloworld/’, ‘newtest.apps.helloworld.views.index’),
    (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.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/edit/$’, ‘newtest.apps.wiki.views.edit’),
    (r’^wiki/(?P<pagename>\w+)/save/$’, ‘newtest.apps.wiki.views.save’),
    (r’^address/’, include(‘newtest.apps.address.urls’)),
    (r’^site_media/(?P<path>.*)$’, ‘django.views.static.serve’, {’document_root’: STATIC_PATH}),
    # Uncomment this for admin:
     (r’^admin/’, include(‘django.contrib.admin.urls.admin’)),
)

你会看到 site_media 就是我将用来存放CSS和图片的地方。那么后面需要一个document_root的参数,这里我使用了一个 STATIC_PATH,它从哪里来呢?它是我自已在settings.py中定义的。在前面有一个导入语句:

from django.conf.settings import STATIC_PATH

从这里可以看到是如何使用settings的,我们完全可以自已定义新的东西,并让它在整个项目中生效。

6. 修改 settings.py

在最后增加:

STATIC_PATH = ‘./media’

也就是在 newtest 下需要一个 media 的目录。

7. 创建 newtest/media 目录

这样根据上面 urls.py 的设置,我们以后将通过 /site_media/XXX 来使用某些静态文件。

为了美化,我想需要一个 CSS 文件来定义一些样式,同时我还想提供一个 django powered 的图片。在这里有官方提供的图标。 于是我下了一个放在了 media 目录下。同时 CSS 怎么办,自已重头写,太麻烦,反正只是一个测试。于是我下载了 django 站点用的 css 叫 base.css 也放在了 media 下面。下面就是对模板的改造。
 
为了通用化,我新增了一个 base.html 它是一个框架,而以前的 address_list.html 是它的一个子模板。这样我们就可以了解如何使用模板间的嵌套了。

8. 创建 templates/base.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Language" content="en-us" />

  <title>Address</title>

  <meta name="ROBOTS" content="ALL" />
  <meta http-equiv="imagetoolbar" content="no" />
  <meta name="MSSmartTagsPreventParsing" content="true" />
  <meta name="Copyright" content="This site’s design and contents Copyright (c) 2005  Limodou." />

  <meta name="keywords" content="Python, Django, framework, open-source" />
  <meta name="description" content="Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design." />

  <link href="/site_media/base.css" rel="stylesheet" type="text/css" media="screen" />
     
 </head>

 <body>
  <div id="container">
   {% block content %}content{% endblock %}
  </div>
  <div id="footer">
   <div>
   <img src="/site_media/djangopowered.gif"/>
   </div>
   <p>&copy; 2005 Limodou. Django is a registered trademark of Lawrence Journal-World.</p>
  </div>
 </body>
</html>

有些代码也是从 django 的网页中拷贝来的。特别要注意的是:

{% block content %}content{% endblock %}

这样就是定了一个可以扩展的模块变量块,我们将在 address_list.html 中扩展它。同时对 CSS 和 django-powered 的图片引用的代码是:

<link href="/site_media/base.css" rel="stylesheet" type="text/css" media="screen" />
<img src="/site_media/djangopowered.gif"/>

前面都是从 site_media 开始的。这样就将使用我们前面在 urls.py 中的设置了。

9. 修改 templates/address/address_list.html

{% extends "base" %}
{% block content %}
<style type="text/css">
h1#title {color:white;}
</style>
<div id="header">
<h1 id="title">通讯录</h1>
</div>
<hr>
<div id="content-main">
    <table border="0" width="500">
    <tr align="right"><td>{% if has_previous %}<a href="/address?page={{ previous }}">上一页</a>{% endif %} {% if has_next %}<a href="/address?page={{ next }}">下一页</a>{% endif %}</td></tr>
    </table>
    <table border="1" width="500">
    <tr><th>姓名</th><th>性别</th><th>电话</th><th>手机</th><th>房间</th></tr>
    {% for person in object_list %}
    <tr><td>{{ person.name }}</td><td>{{ person.gender }}</td><td>{{ person.telphone }}</td><td>{{ person.mobile }}</td><td>{{ person.room }}</td></tr>
    {% endfor %}
    </table>
    <table border="0" width="500">
    <tr><td><form enctype="multipart/form-data" method="POST" action="/address/upload/">
    文件导入:<input type="file" name="file"/><br/>
    <input type="submit" value="上传文件"/>
    </form>
    </td>
    <td><p><a href="/address/output/">导出为csv文件</a></p></td></tr>
    </table>
</div>
{% endblock %}

基本上没有太大的变化,主要是增加了一些div标签,同时最开始使用:

{% extends "base" %}

表示是对 base 的扩展,然后是相应的块的定义:

{% block content %}

{% endblock %}

注意,所有扩展的东西一定要写在块语句的里面,一旦写到了外面,那样就不起作用了。

django 的模板可以不止一次的扩展,但这里没有演示。

10. 都改好了,启动 server 吧

现在你看到的页面是不是象我这样?

重要的版权问题

版权是一个可能我们大多数人都不重视的问题,但在实际生产中,这是一个必须重视的问题。许多东西象CSS,图片,甚至可能是一种布局,设计都有可能有版权,在使用这些东西的时候一定要注意相关的说明。不要给自已造成麻烦。如果你不清楚,建议你去找清楚的人,或与所有者联系。特别是对于开源,版权更是一个很重要的东西,因为这是保护我们的武器,希望每个人都重视。特别是对于正式发布的东西,一定要将版权问题交待清楚。

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

2005年12月30日

现在所有的代码可以通过 svn 来访问了。以后文档有时间也会整理进去,目前还没有。访问地址:

http://cvs.woodpecker.org.cn/svn/woodpecker/zqlib/tangle/limodou/django-stepbystep

同时当前目录不再是 newtest 了,而变成了 django-stepbystep 了,请注意。

上一讲的确很长,但如果看代码你会发现,主要在 model 的调整中,urls.py的工作不作,而连一个 view 的代码都没有写。是不是非常方便呢?

那么让我们来继续完善这个通讯录吧。

现在我想完成的是:增加批量导入和导出功能

为什么要批量导入呢?因为一般情况下,我一定是已经有了一个通讯录文件(象以前我说过的Excel文件),那么现在转到 web 上来,难道要我一条条全部手工录入吗?能不能上传文件,自动插入到数据库中去呢?于是让我们实现一个文件上传的处理吧。

为了简化,我采用csv格式文本文件(这个文件在svn中有一个例子,不然就自行生成好了)。

abc,M,11,11,11,
bcd,M,11,11,11,
ass,M,11,11,11,
dfsdf,F,11,11,11,
sfas,F,11,11,11,

1. 修改 templates/address/addresses_list.html

<h2>通讯录</h2>
<hr>
<form enctype="multipart/form-data" method="POST" action="/address/upload/">
上传通讯录文件: <input type="file" name="file"/><br/>
<input type="submit" value="上传文件"/>
</form>
<table border="1">
<tr><th>姓名</th><th>性别</th><th>电话</th><th>手机</th><th>房间</th></tr>
{% for person in object_list %}
<tr><td>{{ person.name }}</td><td>{{ person.gender }}</td><td>{{ person.telphone }}</td><td>{{ person.mobile }}</td><td>{{ person.room }}</td></tr>
{% endfor %}
</table>

增加文件上传的表单。

2. 创建 apps/address/views.py

#coding=utf-8
# Create your views here.
from django.models.address import addresses
from django.utils.httpwrappers import HttpResponseRedirect
from django.core.extensions import render_to_response

def upload(request):
    file_obj = request.FILES.get(‘file’, None)
    if file_obj:
        import csv
        import StringIO
        buf = StringIO.StringIO(file_obj['content'])
        try:
            reader = csv.reader(buf)
        except:
            return render_to_response(‘address/error’, {’message’:'你需要上传一个csv格式的文件!’})
        for row in reader:
            objs = addresses.get_list(name__exact=row[0])
            if not objs:
                obj = addresses.Address(name=row[0], gender=row[1], telphone=row[2], mobile=row[3], room=row[4])
            else:
                obj = objs[0]
                obj.gender = row[1]
                obj.telphone = row[2]
                obj.mobile = row[3]
                obj.room = row[4]
            obj.save()
           
        return HttpResponseRedirect(‘/address/’)
    else:
        return render_to_response(‘address/error’, {’message’:'你需要上传一个文件!’})

这里有一个 upload 方法,它将使用csv模块来处理上传的csv文件。首先查找姓名是否存在于数据库中,如果不存在则创建新记录。如果存在则进行替换。如果没有指定文件直接上传,则报告一个错误。如果解析csv文件出错,则也报告一个错误。

3. 创建 templates/address/error.html

<h2>出错</h2>
<p>{{ message }}</p>
<hr>
<p><a href="/address/">返回</a></p>

很简单。

4. 修改 apps/address/urls.py

from django.conf.urls.defaults import *

info_dict = {
    ‘app_label’: ‘address’,
    ‘module_name’: ‘addresses’,

urlpatterns = patterns(”,
    (r’^/?$’, ‘django.views.generic.list_detail.object_list’, info_dict),
    (r’^upload/$’, ‘newtest.apps.address.views.upload’),
)

增加一个upload的映射

5. 启动 server 试一试

这里你可能会报错。说是 csv 模块没有 reader 属性。为什么呢?经过排查,是因为我在前面的 csv 的例子中创建了一个叫 csv.py 的文件,它与系统的 csv 模块重复了,因此错误地导入了本地的 csv 模块。这是我是一个疏忽。没办法,我只好将本地的 cvs.py 改为 csv_test.py ,删除已经编译的 csv.py。然后修改 urls.py 中csv为csv_test。这样就没有问题了。

这样导入功能就做完了。那倒出呢?很简单了,参考 csv 的例子去做就可以了。不过,并不全是这样,仍然有要修改的地方,比如 csv.html 模板,它因为写死了处理几个元素,因此需要改成一个循环处理。

6. 修正 templats/csv.html

{% for row in data %}{% for i in row %}"{{ i|addslashes }}",{% endfor %}
{% endfor %}

将原来固定个数的输出改为循环处理。

7. 修改 templates/address/address_list.html

增加一个生成导出的 csv 文件的链接

<h2>通讯录</h2>
<hr>
<form enctype="multipart/form-data" method="POST" action="/address/upload/">
上传通讯录文件: <input type="file" name="file"/><br/>
<input type="submit" value="上传文件"/>
</form>
<hr>
<p><a href="/address/output/">导出为csv格式文件</a></p>
<table border="1">
<tr><th>姓名</th><th>性别</th><th>电话</th><th>手机</th><th>房间</th></tr>
{% for person in object_list %}
<tr><td>{{ person.name }}</td><td>{{ person.gender }}</td><td>{{ person.telphone }}</td><td>{{ person.mobile }}</td><td>{{ person.room }}</td></tr>
{% endfor %}
</table>

8. 修改 apps/address/views.py

#coding=utf-8
# Create your views here.
from django.models.address import addresses
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.core.extensions import render_to_response
from django.core.template import loader, Context

def upload(request):
    file_obj = request.FILES.get(‘file’, None)
    if file_obj:
        import csv
        import StringIO
        buf = StringIO.StringIO(file_obj['content'])
        try:
            reader = csv.reader(buf)
        except:
            return render_to_response(‘address/error’, {’message’:'你需要上传一个csv格式的文件!’})
        for row in reader:
            objs = addresses.get_list(name__exact=row[0])
            if not objs:
                obj = addresses.Address(name=row[0], gender=row[1], telphone=row[2], mobile=row[3], room=row[4])
            else:
                obj = objs[0]
                obj.gender = row[1]
                obj.telphone = row[2]
                obj.mobile = row[3]
                obj.room = row[4]
            obj.save()
           
        return HttpResponseRedirect(‘/address/’)
    else:
        return render_to_response(‘address/error’, {’message’:'你需要上传一个文件!’})
   
def output(request):
    response = HttpResponse(mimetype=’text/csv’)
    response['Content-Disposition'] = ‘attachment; filename=%s’ % ‘address.csv’
    t = loader.get_template(‘csv’)
    objs = addresses.get_list()
    d = []
    for o in objs:
        d.append((o.name, o.gender, o.telphone, o.mobile, o.room))
    c = Context({
        ‘data’: d,
    })
    response.write(t.render(c))
    return response

9. 修改 apps/address/urls.py

from django.conf.urls.defaults import *

info_dict = {
    ‘app_label’: ‘address’,
    ‘module_name’: ‘addresses’,

urlpatterns = patterns(”,
    (r’^/?$’, ‘django.views.generic.list_detail.object_list’, info_dict),
    (r’^upload/$’, ‘newtest.apps.address.views.upload’),
    (r’^output/$’, ‘newtest.apps.address.views.output’),
)

10. 启用 server 测试

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

2005年12月29日

10. 改变界面的语言

用了一会,也许你会希望:能不能有汉化版本的界面呢?答案是肯定的,而且已做好了。

修改 settings.py

把 LANGUAGE_CODE 由 ‘en’ 改为 ‘zh-cn’,TIME_ZONE 建议改为 ‘CCT’

刷新下界面,是不是变成汉字了。

国际化支持在 django 中做得是非常的出色,程序可以国际化,模板可以国际化,甚至js都可以国际化。这一点其它的类似框架都还做不到。而国际化的支持更是 RoR 的一个弱项,甚至在 Snakes and Rubies 的会议上,RoR 的作者都不想支持国际化。但 django 却做得非常出色,目前已经有二十多种语言译文,其中最近我发现加入了中文繁体和日文的译文了。

在增加,删除,修改都做完了,其实还剩下什么呢?显示和查询。那么实现它则需要写 view 和使用模板了。这个其实也没什么,最简单的,从数据库里查询出所有的数据,然后调用模板,通过循环一条条地显示,不错是简单。但是在做之前,先让我们想一想,这种处理是不是最常见的处理方法呢?也许我们换成其它的应用也是相似的处理。如果很多这样的处理,是不是我们需要每次都做一遍呢?有没有通用的方便的方法。答案是有,django 已经为我们想到了,这就是 generic-view 所做的。它把最常见的显示列表,显示详细信息,增加,修改,删除这些处理都已经做好了一个通用的方法,一旦有类似的处理,可以直接使用,不用再重新开发了。但在配置上有特殊的要求。具体的可以看文档 generic_views 。

从这里我有一点想法,我认为 view 这个名称特别容易让人产生误解,为什么呢?因为 view 可以译为视图,给人一种与展示有关的什么东西。但实际上 django 中的 view 相当于一个 controller 的作用,它是用来收集数据,调用模板,真正的显示是在模板中处理的。因此我倒认为使用 controller 可能更合适,这样就称为 MTC 了。呵呵,只是个人想法。

另外,generic-view 产生的意义在于 django 的哲学理含 DRY (Don’t repeat yourself, 不要自已重复),目的是重用,减少重复劳动。还有其它的哲学理含参见 design_philosophies 文档。

因此知道了 view 可以省,但模板却不能省,django 在这点上认为:每个应用的显示都可能是不同的,因此这件事需要用户来处理。但如果有最简单的封装,对于开发人员在测试时会更方便,但目前没有,因此模板我们还是要准备,而且还有特殊的要求,一会就看到了。

对于目前我这个简单的应用来说,我只需要一个简单的列表显示功能即可,好在联系人的信息并不多可以在一行显示下。因此我要使用 django.views.generic.list_detail 模块来处理。

11. 增加 apps/address/urls.py

对,我们为 address 应用增加了自已的 urls.py。

from django.conf.urls.defaults import *

info_dict = {
    ‘app_label’: ‘address’,
    ‘module_name’: ‘addresss’,

urlpatterns = patterns(”,
    (r’^/?$’, ‘django.views.generic.list_detail.object_list’, info_dict),
)

info_dict 存放着 object_list 需要的参数,它至少要两个参数,一个是 app 的名字,一个是 module(表) 的名字,它将根据这两个名字来合成需要的模板路径。也许你注意到了。module_name 是 addresss,怎么是3个’s’。在前面的几讲里我说过,每个 model 都有一个对应的 module 名,在缺省情况下它就是把类名小写后在最后加个 ’s’。这不 Address 小写后加 ’s’ 就成了 ‘addresss’。是不是很头痛。好改一下:

12. 修改 apps/address/models/address.py

#coding=utf-8
from django.core import meta

# Create your models here.
class Address(meta.Model):
    name = meta.CharField(‘姓名’, maxlength=20, core=True, unique=True)
    gender = meta.CharField(‘性别’, choices=((‘M’, ‘男’), (‘F’, ‘女’)), maxlength=1, core=True, radio_admin=True)
    telphone = meta.CharField(‘电话’, maxlength=20, core=True)
    mobile = meta.CharField(‘手机’, maxlength=11, core=True)
    room = meta.CharField(‘房间’, maxlength=10, core=True)
   
    def __repr__(self):
        return self.name

    class META:
        admin = meta.Admin()
        module_name = ‘addresses’       

红色字是新加的,module_name 是专门用来指明模块的名字。这回是 addresses 看上去好多了吧。

但发现,module_name 对于数据库的表名有影响,于是不得已又重新创建了表结构。

13. 重建表结构

manage.py sqlreset address|sqlite3 data.db

然后又重新插入了一条数据。

14. 修改 apps/address/urls.py

于是 urls.py 也要改为:

from django.conf.urls.defaults import *

info_dict = {
    ‘app_label’: ‘address’,
    ‘module_name’: ‘addresses’,

urlpatterns = patterns(”,
    (r’^/?$’, ‘django.views.generic.list_detail.object_list’, info_dict),
)

使用 object_list view 需要的模板文件名为:app_label/module_name_list.html,这是缺省要查找的模板名。

15. 创建 templates/address/addresses_list.html

<h2>通讯录</h2>
<hr>
<table border="1">
<tr><th>姓名</th><th>性别</th><th>电话</th><th>手机</th><th>房间</th></tr>
{% for person in object_list %}
<tr><td>{{ person.name }}</td><td>{{ person.gender }}</td><td>{{ person.telphone }}</td><td>{{ person.mobile }}</td><td>{{ person.room }}</td></tr>
{% endfor %}
</table>

16. 修改 urls.py

将我们的应用的 urls.py include 进去。

from django.conf.urls.defaults import *

urlpatterns = patterns(”,
    # Example:
#    (r’^helloworld/’, ‘newtest.apps.helloworld.views.index’),
    (r’^$’, ‘newtest.helloworld.index’),
    (r’^add/$’, ‘newtest.add.index’),
    (r’^list/$’, ‘newtest.list.index’),
    (r’^csv/(?P<type>\w+)/$’, ‘newtest.csv.output’),
    (r’^login/$’, ‘newtest.login.login’),
    (r’^logout/$’, ‘newtest.login.logout’),
    (r’^wiki/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/edit/$’, ‘newtest.apps.wiki.views.edit’),
    (r’^wiki/(?P<pagename>\w+)/save/$’, ‘newtest.apps.wiki.views.save’),
    (r’^address/’, include(‘newtest.apps.address.urls’)),

    # Uncomment this for admin:
     (r’^admin/’, include(‘django.contrib.admin.urls.admin’)),
)

可以看到 r’^address/’ 没有使用 ‘$’,因为它只匹配前部分,后面的留给 address 中的 urls.py 来处理。

17. 启动 server 看效果吧。

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

6. 增加超级用户

执行:

http://localhost:8000/admin

进入看一看吧。咦,要用户。对,admin 功能是有用户权限管理的,因此一个admin替你完成了大量的工作:用户的管理和信息的增加、删除、修改这里功能类似,开发繁琐的东西。那么我们目前还没有一个用户,因此可以在命令下创建一个超级用户,有了这个用户,以后就可以直接在 admin 界面中去管理了。

manage.py createsuperuser

它会让你输入用户名,邮件地址和口令。

这回再进去看一下吧。

上面已经有一些东西了,其中就有用户管理。但如何通过 admin 增加通讯录呢?别急,我们需要在 model 文件中增加一些与 admin 相关的东西才可以使用 admin 来管理我们的 app 。

7. 修改 apps/address/models/address.py

#coding=utf-8
from django.core import meta

# Create your models here.
class Address(meta.Model):
    name = meta.CharField(‘姓名’, maxlength=6, core=True, unique=True)
    gender = meta.CharField(‘性别’, choices=((‘M’, ‘男’), (‘F’, ‘女’)), maxlength=1, core=True, radio_admin=True)
    telphone = meta.CharField(‘电话’, maxlength=20, core=True)
    mobile = meta.CharField(‘手机’, maxlength=11, core=True)
   
    class META:
        admin = meta.Admin()

红色字是新加的。有了这个东西,你就可以在 admin 中看到 adress 这个 app 了。再到浏览器中看一下是什么样子了。

看见了吧。上面有增加和删除的按钮,先让我们点击一下增加吧。

这个自动生成的界面是不是很不错。增加一条就可以保存起来了。不过我发现当我输入 limodou 时,只能输入 limodo 好象 u 输不进去。为什么?因为我把姓名按汉字算最多6个就够了,一旦我使用英文的名字可能就不够。因此这是一个问题,一会要改掉。

好象有一条记录,但上面怎么看不见。其实还是可以点的。只是不容易。不过我们不用去用鼠标找这个链接到底在哪里,还是要改一下 model 程序。

8. 修改 apps/address/models/address.py

#coding=utf-8
from django.core import meta

# Create your models here.
class Address(meta.Model):
    name = meta.CharField(‘姓名’, maxlength=6, core=True, unique=True)
    gender = meta.CharField(‘性别’, choices=((‘M’, ‘男’), (‘F’, ‘女’)), maxlength=1, core=True, radio_admin=True)
    telphone = meta.CharField(‘电话’, maxlength=20, core=True)
    mobile = meta.CharField(‘手机’, maxlength=11, core=True)
   
    def __repr__(self):
        return self.name

    class META:
        admin = meta.Admin()

红色字是新增的。这次看见了吗?增加了一个 __repr__ 方法。好象使用 __str__ 也可以,不过我也懒得去研究应该使用哪个了。这个方法将在显示 Address 实例的时候起作用。我们就使用某个联系人的姓名就行了。改完后刷新浏览器就可以了。

你记得吗?Model 是与数据库中的表对应的,为什么我们改了 model 代码,不需要重新对数据库进行处理呢?因为只要不涉及到表结构的调整是不用对表进行特殊处理的。不过,我们马上要修改表结构了。

       
7. 修改 apps/address/models/address.py

姓名留短了真是不方便,另外我突然发现需要再增加一个房间字段。

#coding=utf-8
from django.core import meta

# Create your models here.
class Address(meta.Model):
    name = meta.CharField(‘姓名’, maxlength=20, core=True, unique=True)
    gender = meta.CharField(‘性别’, choices=((‘M’, ‘男’), (‘F’, ‘女’)), maxlength=1, core=True, radio_admin=True)
    telphone = meta.CharField(‘电话’, maxlength=20, core=True)
    mobile = meta.CharField(‘手机’, maxlength=11, core=True)
    room = meta.CharField(‘房间’, maxlength=10, core=True)
   
    def __repr__(self):
        return self.name

    class META:
        admin = meta.Admin()

这回表结构要改变了,怎么做呢?

8. 修改表结构

目前 django 没有一个特别的命令可以直接更新表结构,为什么,在 django 看来修改表结构并不是件很容易的事情,主要的问题是原数据怎么办,因此为了使旧的数据可以平滑迁移到新的表结构中,这步操作还是手工来做好一些。但现在我们正在开发中,因此很有可能表结构要经常发生变化,每次手工做多麻烦呀。 django 有一个命令:sqlreset 可以生成 drop 表,然后创建新表的 SQL 语句,因此我们可以先调用这个命令,然后通过管道直接导入数据库的命令行工具中。这里我使用的是 sqlite3 ,因此我这样做:

manage.py sqlreset address|sqlite3 data.db

sqlreset 后面是要处理的 app 的名字,因此它只会对指定的 app 有影响。但这样,这个 app 的所有数据都丢失了。如果想保留原有数据,你需要手工做数据切换的工作。

对于其它的数据库,在数据库命令那里可能是不同的,这个你自已去掌握吧。同时对于 sqlite3 ,有人可能想:直接把数据库文件删除了不就行了。但是你一定要清楚,如果存在其它的 app 的话,它们的数据是否还有用,如果没用删除当然可以,不过相应的 app 都要再重新 install 一遍以便初始化相应的表。如果数据有用,这样做是非常危险的,因此还是象上面的处理为好,只影响当前的 app 。

9. 进入 admin

我们可以再次进入 admin 了,增加,删除,修改数据了。

敢问路在何方,路在脚本。如果你坚持下来,一定会有收获的。

直到目前我们已经学了:

  • settings.py的设置
  • url dispatcher
  • 模板
  • session
  • app
  • model

其实在某些方面,使用 django 还可以更加方便。当然我们还有许多东西没有学,一点点跟着我学吧。

我有一个通讯录,它是保存在 Excel 文件中的,我不想每次到目录下去打开它,我希望用 django 做一个web上的简单应用,如何做呢?

1. 创建 address app

cd apps
django-admin.py startapp address

这样就创建好了 address 相关的目录了。

2. 修改 apps/address/models/address.py

#coding=utf-8
from django.core import meta

# Create your models here.
class Address(meta.Model):
    name = meta.CharField(‘姓名’, maxlength=6, core=True, unique=True)
    gender = meta.CharField(‘性别’, choices=((‘M’, ‘男’), (‘F’, ‘女’)), maxlength=1, core=True, radio_admin=True)
    telphone = meta.CharField(‘电话’, maxlength=20, core=True)
    mobile = meta.CharField(‘手机’, maxlength=11, core=True)

这回 model 复杂多了。在上面你可以看到我定义了四个字段:

name 是姓名。在 django 中每个字段都可以有一个提示文本,它是第一个参数,如果没有则会使用字段名。因此我定义的每个字段为了方便都有一个对应的汉字提示文本。core=True是什么,这是一个新东西。它表示这个字段可以在 admin 中进行处理。因为本节主要是讲 admin 的使用。admin 是 django 提供的一个核心 app(既然是 app 就需要安装,一会就看到了),它可以根据你的 model 来自动生成管理界面。我为什么要用它,因为有了这个管理界面,对于通讯录的增加、删除、修改的处理界面完全可以通过 admin 来自动生成,我不用自已写。不相信吗?我们就会看到了。

那么 admin 到底可以带来些什么好处呢?它的功能很强大,不仅界面漂亮,还能对数据提供操作记录,提供搜索。特别是它是在用户权限控制之下,你都可以不用考虑安全的东西了。并且它本身就是一个非常好的学习的东西,特别是界面自动生成方面,它的经验可以用在我们自已的定制之中。当然,你许你用不上 admin ,它的确有一定的适应范围,不过对于大部分工作来说它可能足够了。对于那些交互性强的功能,你可能要自已实现许多,对于管理集中,主要以发布为主的东西,使用它可以节省你大量的时间。至于怎么使用,你要自已去权衡。但这一点对于快速实现一个 web 应用,作用非常大,这是 django 中的一个亮点。

3. 修改 settings.py

INSTALLED_APPS = (
    ‘newtest.apps.wiki’,
    ‘newtest.apps.address’,
    ‘django.contrib.admin’,
)

这里我们加入了两个app,一个是 address,还有一个是 django.contrib.admin。admin也是一个应用,需要加入才行,后面还要按添加 app 的方式来修改URL映射和安装 admin app。这些与标准的app的安装没有什么不同。

4. 安装 admin app

manage.py install admin

这样将在数据库中创建 admin 相关的表。

5. 修改 urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns(”,
    # Example:
#    (r’^helloworld/’, ‘newtest.apps.helloworld.views.index’),
    (r’^$’, ‘newtest.helloworld.index’),
    (r’^add/$’, ‘newtest.add.index’),
    (r’^list/$’, ‘newtest.list.index’),
    (r’^csv/(?P<type>\w+)/$’, ‘newtest.csv.output’),
    (r’^login/$’, ‘newtest.login.login’),
    (r’^logout/$’, ‘newtest.login.logout’),
    (r’^wiki/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/edit/$’, ‘newtest.apps.wiki.views.edit’),
    (r’^wiki/(?P<pagename>\w+)/save/$’, ‘newtest.apps.wiki.views.save’),

    # Uncomment this for admin:
     (r’^admin/’, include(‘django.contrib.admin.urls.admin’)),
)

缺省的 urls.py 中已经带了,不过是一个注释,把注释去掉就好了。这里要注意,它使用了一个 include。因此对于这种URL的解析是分段的,先按 r’^admin/’ 解析(这里没有$),匹配了则把省下的部分丢给 ‘django.contrib.admin.urls.admin’ 去进行进一步的解析。使用include 可以方便移植,每个 app 都可以有独立的 urls.py ,然后可以与主 urls.py 合在一起使用。配置起来相对简单。而且可以自由地在主 urls.py 中修改应用URL的前缀,很方便。

6. 在 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>

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

7. 编辑 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>

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

8. 修改 urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns(”,
    # Example:
#    (r’^helloworld/’, ‘newtest.apps.helloworld.views.index’),
    (r’^$’, ‘newtest.helloworld.index’),
    (r’^add/$’, ‘newtest.add.index’),
    (r’^list/$’, ‘newtest.list.index’),
    (r’^csv/(?P<type>\w+)/$’, ‘newtest.csv.output’),
    (r’^login/$’, ‘newtest.login.login’),
    (r’^logout/$’, ‘newtest.login.logout’),
    (r’^wiki/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/$’, ‘newtest.apps.wiki.views.index’),
    (r’^wiki/(?P<pagename>\w+)/edit/$’, ‘anewtest.pps.wiki.views.edit’),
    (r’^wiki/(?P<pagename>\w+)/save/$’, ‘newtest.apps.wiki.views.save’),

    # Uncomment this for admin:
#     (r’^admin/’, include(‘django.contrib.admin.urls.admin’)),
)

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

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

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

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

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

下面你可以运行了。

9. 启动 server

进入 http://localhost:8000/wiki

首先进入这个页面:

然后你点编辑,则进入FrontPage的编辑界面:

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

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

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

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

本例子为了测试方便将代码打包到传到了 啄木鸟社区 上了,可以从这里下载。下载教程代码。此包包含所有以前的例子的代码。

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

5. 修改 apps/wiki/views.py

#coding=utf-8
from django.models.wiki import wikis
from django.core.template import loader, Context
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.core.extensions import render_to_response

def index(request, pagename=""):
    """显示正常页面,对页面的文字做特殊的链接处理"""
    if pagename:
        #查找是否已经存在页面
        pages = wikis.get_list(pagename__exact=pagename)
        if pages:
            #存在则调用页面模板进行显示
            return process(‘wiki/page’, pages[0])
        else:
            #不存在则进入编辑画面
            return render_to_response(‘wiki/edit’, {’pagename’:pagename})
       
    else:
        page = wikis.get_object(pagename__exact=’FrontPage’)
        return process(‘wiki/page’, page)
   
def edit(request, pagename):
    """显示编辑存在页面"""
    page = wikis.get_object(pagename__exact=pagename)
    return render_to_response(‘wiki/edit’, {’pagename’:pagename, ‘content’:page.content})

def save(request, pagename):
    """保存页面内容,老页面进行内容替换,新页面生成新记录"""
    content = request.POST['content']
    pages = wikis.get_list(pagename__exact=pagename)
    if pages:
        pages[0].content = content
        pages[0].save()
    else:
        page = wikis.Wiki(pagename=pagename, content=content)
        page.save()
    return HttpResponseRedirect("/wiki/%s" % pagename)

import re

r = re.compile(r’\b([A-Z][a-z]+[A-Z][a-z]+)\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))
   
代码有些长,有些地方已经有说明和注释了。简单说一下:

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

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

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

page = wikis.get_object(pagename__exact=’FrontPage’)

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

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

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

注意,为了发贴所有半角大括号都改成了全角大括号,因此如果想运行的话,要进行替换处理。

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

下面我们按照 TurboGears 的 20 分钟 wiki 的例子仿照一个,我们要用 django 来做 wiki。我不会按 TurboGears 的操作去做,只是实现一个我认为的最简单的 wiki。

现在我的要求是:做一个简单的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用来生成界面。

1. 创建wiki app

app 一般都放在 project 目录下的 apps 子目录中,它是一个独立的目录,初始可以通过命令行工具来进行。

cd apps
django-admin.py startapp wiki

这样在 apps 目录下就会创建一个 wiki 子目录,它有一个 __init__.py 文件,表示它是一个包。还有一个 views.py 用来放它的 view 的代码。有一个 models 目录,下面有一个 __init__.py 文件。说明它也是一个包。这一切都是为了在后面可以方便以模块的方式来导入各个 Python 文件。其实目录结构并不是重要的,重要的是在相关的配置中可以让 django 找得到就行。更早一些的 django 版本目录比这里的还要多,还要让人混乱:)

2. 编辑 apps/wiki/models/wiki.py

from django.core import meta

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

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

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

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

一旦你定义好了 model ,在运行时,django 会自动地为这个 model 增加许多数据操作的方法(这里 django 使用了 metaclass 的处理,这些都是细节了)。关于 model 和 数据库操作API的详细内容参见 models 和 db-api 的文档。

3. 修改 settings.py, 安装 app

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

第一步,修改settings.py

INSTALLED_APPS = (
    ‘newtest.apps.wiki’,
)

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

第二步,执行(在newtest目录下)

manage.py install wiki

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

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

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

进入  newtest

set PYTHONPATH=d:\test
set DJANGO_SETTINGS_MODULE=newtest.settings

注意 PYTHONPATH 需要设为 newtest 的父目录,我的 newtest 的全路径为:d:\test\newtest 。你的可能与我不同,请注意修改。不这样做不行,manage.py只是在用它的时候才起作用,但在命令行下使用 Python 则需要手工设置。因此可以把这种设置写成一个批处理就好了。

进入 Python

>>> from django.models.wiki import wikis
>>> page = wikis.Wiki(pagename=’FrontPage’, content=’Welcome to easy Wiki’)
>>> page.save()
>>> wikis.get_list()
[<Wiki object>]
>>> p = wikis.get_list()[0]
>>> p.pagename
‘FrontPage’
>>> p.content
‘Welcome to easy Wiki’

这里有一点非常重要的东西,那就是 from django.models.wiki import wikis ,你知道这个 wikis 从哪里来的吗?我记得从来没有定义过呀。这就是 django 的 model 中非常将人糊涂的地方。这个要从 model 说起。每个 model 表示一个表,那么我们在操作它时会以两种方式:一种是操作集合,另一种是操作对象,也就是一条记录。而在 django 中操作对象是直接使用你所定义的类,但操作集合呢?它不象我们认为的那样直接使用类就可以,而是 django 自动创建了一个叫 wikis 的东西,你要使用它来操作集合。同时要用它来使用我们定义的类。因此,想操作集合,如得到一个对象列表,可以 wikis.get_list() ,如果要创建一个对象,则需要 wikis.Wiki() 来创建。这一点的确很讨厌,不过在最新的文档 Removing the magic 中有新的用法,可以让我们的头脑简单一些,大家有兴趣自行阅读吧,毕竟还没有正式使用。这个 wikis 在 django 中是一个 module ,而且你可以控制它的名字,这一点在 model-api 中有描述,内容是这样的:

module_name

    The name of the module:

    module_name = "pizza_orders"

    If this isn’t given, Django will use a lowercased version of the class name, plus "s". This "poor man’s pluralization" is intentional: Any other level of magic pluralization would get confusing.

也就是说,django 会自动为每一个类生成一个 module ,而且这个module是类名的小写+’s’。因此如果你的类名为:address,则么module_name 就是addresss。是不是很烦人呢?但在META(也会改掉)中可以修改这个缺省的名字,比如改为 addresses 。

也许你仍然糊涂,没办法,简单地说:每个类都会自动生成一个 module ,它的名字是:类名小写+’s’。在处理集合时要使用module,在创建记录时要使用类。在某些自动化场合,django 会要求你提供 module 的名字,因此它非常重要。

(未完待续…)