2006年12月31日

Generic Relation 并不是一个新出的东西,不过此前我一直没有使用过。现在为了将 SharePlat 中的某些功能独立出来,我开始使用它了。在 django 源码中的文档中并没有 Generic Relation 的说明,你可以从 django 的网站找到这一文档

在文档中描述得很清楚。基本上有两种使用:GenericForeignKey和GenericRelation。比如我有一个Topic表,它用来保存讨论主题。然后有一个Comment表,它用来保存回复。为了使用Comment通用,Comment并不直接保存到Topic的关系。这两个表的Model可以为:

from django.contrib.contenttypes.models import ContentType

class Topic(models.Model):
    title = models.CharField(maxlength=200)
    content = models.TextField()
    comments = models.GenericRelation(Comment)

class Comment(models.Model):
    content = models.TextField(default=”)
    content_type = models.ForeignKey(ContentType)
    object_id = models.IntegerField()
    content_object = models.GenericForeignKey()

这是两个简化后的Model。这里体现了两种用法。

先看Comment。Comment中使用GenericForeignKey()来指向其它的Model实例。为了使用它,你还需要在Model中定义content_type和object_id才可以。其中content_type指向ContentType这个Model。在示例的最上面可以看出,它是定义在django.contrib.contenttypes.models中的一个Model。是不是感觉麻烦,没办法。想当初我在 Woodlog 中也想实现类似的功能,当时我是使用了一个字符串,包括了Model名和记录的id值,通过’/'来分隔。所以处理时,要每次拆分。比这个还要麻烦。其实仔细想一想这是没有办法的事。在django中,你如何定位一条记录?一般要三个值:appname, modelname和object_id。在ContentType就是保存了appname和modelname。因此使用GenericForeignKey你就只要两个值了。但GenericForeignKey本身还是要定义一下的。结果还是三项。使用其实还是挺简单的。看文档就清楚了。

再看Topic。它定义了一个GenericRelation(Comment)。这样你可以通过instance.comments.all()来访问instance对应的所有的Comment实例。当然这个定义并不是必须的,如果不定义,你需要手工去Comment中查找,比如你已经有了一个topic的实例,现在要查找它的回复:

ctype = ContentType.objects.get_for_model(topic)
Comment.objects.filter(content_type__pk=ctype.id, object_id=topic.id)

同时在一个Model中可以定义多个GenericForeignKey,因为GenericForeignKey在定义时可以指定content_type和object_id字段的名字,缺省是这两个,如果指定新的名字,就可以定义新的GenericForeignKey了。如:

    content_type = models.ForeignKey(ContentType, related_name=’content_type’)
    object_id = models.IntegerField()
    content_object = models.GenericForeignKey()
    own_type = models.ForeignKey(ContentType, related_name=’own_type’)
    own_id = models.IntegerField()
    own_object = models.GenericForeignKey(ct_field=’own_type’, fk_field=’own_id’)

使用Generic Relation可能还有一些功能是不够充分的,在 Django 的邮件列表中有人提过。我发现的就是 create_or_get()好象不行。

不管怎么样,如果你想让你的App可以处理不同的Model,如你的App是Tag, Comment等,可以考虑GenericForeignKey和GenericRelation来处理,可以简化你的处理。

Firebug 功能很强大,别的不说,说一说ajax调试吧。Ajax调试是很麻烦的事,如果后台执行错误,前面可能什么结果都看不到。在 django 中调试就是如此。如果你打开 django 的 DEBUG 选项,当后台执行出错时,一般 django 会弹出一个出错页面,这个页面还是不错。不过当转为 Ajax 的调用时,一旦出错,你就只能在 firebug 中看到出错,同时可以看到长长的reponse信息。不过我发现,当点出reponse时,再点右键可以看到有一个 Open in New Tab 菜单,然后执行,好了,django的出错页面可以看到了。想想以前真是很麻烦,要么去查看html代码,找到相应的位置,要么拷贝下来存成一个文件再打开,想不到 firebug 已经做好了。

2006年12月30日

如果你上过 djangoIRC 聊天频道,你会看到在频道的开始有一个 dpaste.com 的网站,在IRC聊天时,有时人们会粘贴一些链接,这些链接都指向一个 dpaste.com 的网站。它是做什么的呢?它是一个可以让你随意粘贴文本的网站,主要是用来与别人方便交流代码。因为IRC并不适合贴大段的程序。缺省情况下 dpaste.com 网站会保存 30 天你的信息。使用它非常简单,最简单的情况你只要输入一个 code 字段就行了。然后再选择一下你的代码想使用的语法高亮就行了。它会自动为你生成一个 id ,然后就可以与别人交流了。

这个网站是使用 django 开发的,是作者作为一个练习的产物。它的网站源码也可以下载。我想学过django的人自已开发一个类似的东西学是非常简单的。它的view全部使用了generic view方法,这样的话,你根本不需要写一行view程序,你要做的只是设计一些模板,在urls.py中传入到generic view方法中去就行了。不过,它的网站源码并不完整,比如urls.py没有,有些功能可能在使用时才会发现没有源码。

语法高亮它使用了一个叫 Pygments 的模块,它支持许多种语言的语法高亮渲染,非常棒,不过我还没有用过。不过我已经看到有几个 django 的项目在使用它了。

2006年12月29日

This subject is not a bug report or enhancement proposal, just a tip, and I’v began to use it.

What I want to say?

I think many django users know that you can use decorator in the view, just like:

@check_userid
def list(request, user_id):
   …

So using decorator, you can split individual functionalities into pieces, and reuse them as decorators, or simple as:

def list(request, user_id):
  …
list = check_userid(list)

And for now, in the trunk, you can even use function objects in the urls, just like:

from django.conf.urls.defaults import *
from yourapp.views import func

urlpatterns = patterns(”,

   (r’^$’, func),
)

So this a great thing!

We can make many tricks on it. And I want to introduce how to use decorator on it.
Very simple!

Say you put check_userid in a module named decorator.py, and the urls.py could be:

from django.conf.urls.defaults import *
from yourapp.views import func
from decorator import check_userid

urlpatterns = patterns(”,

   (r’^$’, check_userid(func)),
)

So you see, you can move the decorator from the views into urls.py. And I think  sometimes urls.py is the very suitable place do these things, just like: permission checking, environment checking, exception handling, even we can think about template rendering in here.

Some examples

I’v finished several decorators, for example:

class HttpRedirectException(Exception):pass

# render_template
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.http import HttpResponseRedirect

def template(templatename):
   """
   render the func’s result into a template
   """

   def _render(func=None):
       @exceptionhandle
       def _f(*args, **kwargs):
           if func:
               result = func(*args, **kwargs)
           else:
               result = {}
           return render_to_response(templatename,
context_instance=RequestContext(args[0], result))
       return _f
   return _render

from django.conf import settings

def exceptionhandle(func):
   """
   Catch Special Exception and deal with them
   """
   def _f(*args, **kwargs):
       try:
           return func(*args, **kwargs)
       except HttpRedirectException, e:
           return HttpResponseRedirect(str(e))
   return _f

If you use template decorator, you can just return your data from the view function, for example:

@template(‘template.html’)
def list(requst):
   objs = User.objects.all()
   return objs

And if you want to use decorator in urls.py you can write it:

from django.conf.urls.defaults import *
from yourapp.views import list
from decorator import template

d_list = template(‘template.html’)
urlpatterns = patterns(”,

   (r’^$’, d_list(list)),
)

So you may ask what’s the benefits of it. So I think there are many benefits:

  1. You can make the control centralized management
  2. Reuse is more easy, because the common app can only return the data but not the rendered html text, so change the output will be easy.
  3. others, I don’t think so much

Because you can only return data, but not html text, so you can use decorator to render the result with template or json(ajax support), so that it can support both html and json.

From the above template decorator, you can see it invoke another exceptionhandle decorator, and this decorator can catch a HttpRedirectException exception, so you can write your view just like:

@template(‘template.html’)
def list(request):
   if success:
       #return HttpResponseRedirect(‘url’)
       raise HttpRedirectException, ‘url’
   else:
       return errorinfo

So you can use template to deal with two type different output: redirect and normal render output. Of course, template just my implementation and I just want to use it to describe some example. Just some tips, you can do anything what you want if django supports it.

I hope you have fun with decorator.

后记,这是我发在 Django 邮件列表中的,不知道会有正面还是负面的反应。英文不好,别介意。上述代码在 SharePlat 中都可以找到。在utils/decorator.py中,还有其它的一些decorator。

2006年12月27日

昨天写了一个用于 SharePlat 的 NewMessage Model,想在manage.py shell中试一试,但发现通过 User 无法找到 newmessage_set  对象。但是 auth 自带的 message_set 却可以找到(在contrib/auth中,django 自带了一个Message的 Model ,不过我不想用它,因为内容有些少)。后来想到,可能是因为Message Model与User是定在一个文件中的,因此可能在处理时被同时导入就可以找到了。于是我在命令行上又将NewMessage这个Model导入,结果可以了。

感觉很奇怪。因为感觉manage.py shell在启动时也应该会将所有的app导入啊,不过我没有细研究。启动服务器测试就没有问题。

现在写程序首先想得最多的就是:如何使结构简单,如何使代码重用。因此在 SharePlat 的设计上许多地方都在考虑重用的问题。其实在任何一个项目中,这都是需要思考的一个问题,不然不会有那么多的模式,公共组件的东西了。那么如何实现 django 的模板重用呢?为了重用,首先要将模板分解,转为可以重用的“小块”,然后再考虑如何通过一些参数的控制改变这些“小块”的内容,当然有些是不需要的。在 django 中提供了几种方式的重用,下面我简单地说一下我的理解。

一、文件级别的重用

我们可以把可重用的内容分解成文件,然后使用它们。那么基本上有两种方式:include 和 extends。

include就象C中的include处理一样,相当于直接把其它的文件直接装入到当前模板中来。在include中可以设定一些变量,这些变量将使用包含模板的环境。比如A要include B,则B将使用A中的变量。那么当B中的变量与A中的不一致怎么办。可以使用我写的几个扩展tag来实现不同的处理。先设定一下场景:A中有a,b两个变量,B中需要c,d两个变量。

1. 使用ExprTag/CatchTag

它可以定义新的变量在模板中。因此只要在include B之前按B的要求生成相应的变量就可以了。举例:

{% expr a as c %}
{% expr b as d %}
{% include "B" %}

{% catch as c %}{{ a }}{% endcatch %}
{% catch as d %}{{ b }}{% endcatch %}
{% include "B" %}

2.使用CallTag

它可以象include一样的工作,但是可以传入参数,这样你可以直接把不同的变量名转为可供B使用的变量名,使用就象一个函数调用。举例:

{% call "B" with c=a, d=b %}

再有就是使用extends了。它是象类的继承一样的方式来工作。它与include差别很大。比如B将扩展A,那么如果在B中有需要重新定义的内容,首先需要在A中使用{% block name %}{% endblock %}来定义一个块。然后在B中才可以重新来定义这个块。那么这个块变量与模板变量是不同的,想要替换它,只能通过extends方式来替换掉。因此上extends更多的是用在结构上的处理,如布局上。而include更多的是在内容上的处理。block所定义的块,可以认为是一个缺省值,在继续模板中可以通过block的再定义来替换它。因此block定义与模板变量的差别就是:block定义是直接存在于模板中的,它的内容相当于一个缺省值,可以被其它的block替换(但要注意,你不能在一个模板中定义两个同名的block)。因此在include中也可以实现相应的效果,比如检查模板变量是否存在,不存在则使用缺省值,而且在include前,定义了模板变量的话,就可以替换掉缺省值了。如:

{% var|default:defaultvalue%}

当var不存在或为假时,将使用defaultvalue值。这个defaultvalue可以为字符串,或其它的变量值。因此,如果你的缺省值比较复杂,可以使用 ExprTag 来定义这个缺省值,只要名字别与变量名相同就行了,如:

{% expr "aaa" as defaultvalue%}
{% var|default:defaultvalue %}

在使用上include与extends的区别:

使用include时,如果A include B,你要处理A模板。使用extends时,如果B extends A,你要处理B模板。

extends提供了一种缺省结果,因此在继承模板中可以只定义需要修改的部分,因此它的重用是基于整体的重用。你会把一个很完整的东西继承下来,然后修改不合适的地方。而include的重用是局部的重用,整体的东西是你来控制的,只是把一些局部的东西用可重用的片段来处理。不过使用include也可以实现extends类似的效果,比如A定义了整体的结构,只是在使用block的地方,都使用 var|default:"XX" 的方式进行了处理。然后B想要继承A模板,它只要include "A",然后在此之前再定义相应的模板变量用来替换缺省值就行了。当然也可以使用CallTag来处理。

二、片段级别的重用

其实前面也有所说明。这里我的意思是想说在一个模板中,可能还存在部分有相同或相似的地方,也可以通过一些简单的扩展Tag来实现局部片段的重用。如一个url的前缀,可能比较复杂,但同时需要在多个地方出现,这样为了减少重复定义,可以使用 ExprTag 或 CatchTag 来处理。

ExprTag 与 CatchTag 的区别是:

ExprTag 中是一个表达式,只要是符合python语法的表达式都可以,它还可以从Context中取得模板变量,然后当存在 as 子句时,将把表达式的值保存到 as  后面的变量名中,并将这个变量保存在 context 中,因此这个变量就成为模板变量。如果有重名,它将替换原有的变量。如果不存在as子句,则表达式的值将直接输出到模板中,但要求表达示的结果可以转为str对象。

CatchTag不是一个单个的tag,它有一个结束tag {% endcatch %},它可以把标签内的内容进行渲演处理,然后保存到一个变量中。因此它的内容可以是一段模板的处理,这段处理可以有其它的标签。所以对于大段的内容会更为方便。

通过这两个Tag,你可以把一些重复性的内容存到一个变量中,然后在需要时使用,不用再重复计算了。

上述几个标签代码都可以在 SharePlat 中找到,位置在 apps/common 这个app中。使用时只要先安装这个app,然后使用{% load utiltags %}就可以了。

同时在django的wiki中也有,分别在ExprTag, CallTag, CatchTag。你要组织自已的templatetags目录,然后保存到一个文件中去,按照django对自定义tag的要求去处理。

2006年12月22日

在最近的 SharePlat 的项目中,我想使用一些 decorator 来进行处理,有些 decorator 需要一些参数,因此上生成的 decorator 就有一些复杂,比如:

def template(templatename):
    """
    render the func’s result into a template
    """
   
    def _render(func):
        def _f(*args, **kwargs):
            result = func(*args, **kwargs)
            return render_to_response(templatename, context_instance=RequestContext(args[0], result))
        return _f
    return _render

从上面可以看到template带参数,按decorator的生成要求,它要再返回一个decorator函数才行,因此上又定义了两个函数:_render和_f。而返回_render则是一个decorator函数。

那么不带参数的话,可以简化一些:

def check_userid(f):
    """
    render the func’s result into a template
    """
   
    @decorator.errorhandle
    def _f(*args, **kwargs):
        request = args[0]
        object_id = kwargs['object_id']
        try:
            person = User.objects.get(pk=int(object_id))
        except User.DoesNotExist:
            raise Exception, "用户 ID (%s) 不存在!" % object_id
        return f(*args, **kwargs)
    return _f

这个check_userid没有多作的参数,因此,在它内部只定义了一个函数。但是因为这个函数的功能是为了检查参数中给定的object_id是否存在,所以需要从传入的函数f中得到相应的参数进行处理。那么f的标准形式为:

@check_userid
def user_detail(request, object_id):

那么在check_userid需要得到request和object_id两个值。因此上在_f函数中可能通过args[0]来得到request。但是你不能使用args[1]来得到object_id。为什么,因为django在调用user_detail时,会使用关键字的方式来传入object_id,也就是在_f中你会在kwargs中看到’object_id’这个值。因此,object_id需要通过kwargs['object_id']来得到。这一点需要注意。虽然定义user_detail方法没有关键字参数,但是django在调用时会使用关键字参数,所以造成后面的参数取得会有问题。

update: 2006/12/23

后来想到为什么 django 会将object_id 以关键字参数的方式传入呢?原因还在于urls.py中的定义,如果你使用了 (?P<name>)的形式,那么 django 会自动使用关键字参数来调用views方法,如果只是简单的正则式则不会。所以建议在定义url时,模式最好一致,以减少不必要的麻烦。

今天在PyPI上看到这个模块,它是用来进行解析处理的,其实它并不是很复杂。它的主页上还没有东西,可以下载它的源码包看一看文档和示例。地址

它主要的用处就是:

使用Python正则式定义一系列的token,这些token有简单的,可以通过一个正则式定义的,可以有组合的,可能有一个分析的过程。每个Token其实是一个callable对象,它接受两个参数,一个是parser对象,一个是cursor对象。如果你直接使用Token类来创建一个token的话,它还可以有一个callback,可以用来作特殊的处理。

举一个小例子:

from ZestyParser import *
import re
import sys

T_HELLO = Token(‘hello’, re.compile(‘hello’, re.IGNORECASE))
T_SP = Token(’space’, ‘\s+’)
T_WORLD = Token(‘world’, re.compile(‘world’, re.IGNORECASE))

tokens = [T_HELLO, T_SP, T_WORLD]
p = ZestyParser(file(sys.argv[1]).read())

for t in p.iter(tokens):
    print t

文档中建议使用 from ZestyParsr import *的形式。上面定义了三个token,可以看到,可以使用re.compile()也可以直接使用字符串。这里有一个问题就是,如果你使用了字符串,Zesty在将字符串转化为compile对象时,会自动使用re.DOTALL标志,有可能不是你想要的。

ZestyParser()接受一个字符串,然后它返回一个parser对象。这个对象有一些方法,其中有scan()和iter()。scan可以扫描一个token列表,但只处理一次。只iter将自动循环,直到找不到为止。

不过通过查看源代码,我发现ZestyParser使用了match方式来匹配,这样使得它的处理是从给定的字符串的开头进行的,因此一旦出现不匹配,将不再继续处理。所以你不能用它来查找某个东西。所以,如果你想用它把一个完整的内容全部分析出来的话,那你需要把所有要分析的内容定义出来。另外,它在scan时,是对token列表依次进行匹配,只要找到一个匹配就返回,因此如果你的正则式有包含的情况,一定要考虑好它的顺序。

2006年12月18日

昨天晚上参加了豆瓣的年底答谢会,已经有人记录此事。去的一些网络名人有Keso, 车东等,我反正都不认识,只是在网上看过。人是不少,大概有近30人吧,我没有数。算一算,加上豆瓣在内的CPUG的成员(3位:xyb, albertlee, qiangninghong),我,zoom.quiet, dreamingk,大家倒是很谈得来,不过主要还是技术。不过不象我想象得举办成一个晚会,只是一个聚会,只是大家随意得聊一聊。许多人都不认识,加上我性格内向,不会主动与人结识。

豆瓣给每个到场的人准备了一个标子,印有每个人的网名和豆瓣的名字,本来想拿到单位去,不过想一想也没几个知道的,想要炫耀的话,还要费一番唇舌,只好作罢。

2006年12月15日

头太晕在我的blog中留言说我的实现方法无法解决第一次手工输入,以后一直使用这个正确的值来攻击的问题,我想一想的确如此。看来完全依赖客户端是不够的,还是要在后台做一些工作。为了让后台的工作尽量少,我不想单独建表来象session一样的处理。我也不想去产生一个id与word进行对应,因为生成id是一个问题,为了保证不重复,要通过循环来实现,感觉不好。于是我想不如将key的生成时间也写在key中,这样在后台我只要判断是否超时,就可以让这个key失败。但这也只是解决了key的长期有效的问题,无法解决在短时间内攻击的问题。那么我想可以利用cache的机制,一旦key中的word验证有效,并且没有失效,那么先在cache中查找是否存在,如果不存在则说明没有验证过,然后在cache中设置一个word值既可。这样,下次再次校验相同的word时,因为cache中已经有了这个值,所以验证失效。我想这样应该可以解决问题。改动的代码如下:

#django binding
from django.conf import settings
from django.core.cache import cache
import base64
import md5
import time

TIMEOUT = 5*60

def create_key():
    word = valid_obj().word
    date = time.strftime("%Y%m%d%H%M%S")
    d = md5.new(word + date + settings.SECRET_KEY).hexdigest()
    w = base64.standard_b64encode(word)
    return w + date + d

def get_word(key):
    try:
        d = key[-32:]
        date = key[-46:-32]
        w = key[:len(key)-46]
        #judge the time
        t = time.mktime(time.strptime(date, "%Y%m%d%H%M%S"))
        if 0 <= time.time() – t <= TIMEOUT:
            word = base64.standard_b64decode(w)
            nd = md5.new(word + date + settings.SECRET_KEY).hexdigest()
            if nd == d:
                return word
    except:
        import traceback
        traceback.print_exc()
        pass
    return False

def valid_key(key, word):
    k = get_word(key)
    if k:
        flag = k == word
        if flag:
            #check if the cache has the key, if has the key should be invalid
            if cache.get(word):
                return False
            else:
                #set to cache
                cache.set(word, 1, TIMEOUT)
                return True
    else:
        return False

完整的代码可以在 SharePlat 中找到。utils/validcode.py

update: 将date也作为生成md5的一部分。