增加了用于用户发布的表单之后,你的代码分享应用近乎完成。还有三个特性需要实现。之后你需要增加几个最后的视图来完成应用。现在就开始吧。
10.1 Snippets 书签标记
当前,应用的用户可以通过浏览器中的书签或者发送书签到像Delicious这样的网络服务来追踪他们喜欢的snippet。但是,最好是给每个用户一个直接在站点上追踪他们喜欢的代码列表。这会极大的减少每个用户的通用书签,并且将会提供一个有用的网络度量——被标记最多的代码片段——能提供公共的追踪和显示的功能。
为了支持这一特性,首先需要一个模型来表示这个用户的书签。这是一个十分简单的模型,因为它所要做的只是追踪几条信息:
- 书签所属的用户
- 用户标记的代码段
- 用户标记代码段的时间和日期
你可以打开cab/models.py并向其中添加一个新的由三个字段组成的Bookmark模型来实现:
1 class Bookmark(models.Model):2 snippet = models.ForeignKey(Snippet)3 user = models.ForeignKey(User,related_name='cab_bookmarks')4 date = models.DateTimeField(editable=False)5 6 class Meta:7 ordering = ['-date']8 9 def __unicode__(self):
10 return "%s bookmared by %s" % (self.snippet,self.user)
11
12 def save(self):
13 if not self.id:
14 self.date = datetime.datetime.now()
15 super(Bookmark,self).save()
这里只用了一个新个特性,指向User模型的外键中使用的relateed_name参数。实际上当你为User创建了一个外键,Django将会为每个User对象添加一个新的属性,用来访问每个用户的书签。默认情况下,这个属性会根据你的模型名Bookmar被命名为bookmark_set。例如,你可以这样查询一个用户的书签:
1 from django.contrib.auth.models import User
2
3 u = User.onject.get(pk=1)
4 bookmarks = u.bookmark_set.all()
但是,这会造成一个问题:如果你使用的应用带有一个书签系统,并且如果那个应用将其命名为Bookmar,就会产生一个命名冲突,因为User的bookmark_set属性不能同时被两个不同的模型引用。
解决办法就是对ForeignKey使用related_name参数,可以让你手动的在User中设置一个新属性,用于访问书签。在本例中,你将使用cab_bookmarks。因此一旦这个模型被安装并且在数据库中已经有了一些书签的时候,你可以这样运行查询:
1 from django.contrib.auth.models import User
2
3 u =User.objects.get(pk=1)
4 bookmarks = u.cab_bookmarks.all
总的来说,任何时候当你创建一个关联模型的关系时,使用related_name是一个很好的做法。
同样需要注意的是,因为用户会完全通过公共界面视图管理他们自己的书签,所以不必将Bookmark模型加入管理界面。
接下去运行manage.py syncdb来安装Bookmark模型到数据库中。在提醒一遍,syncdb足够聪明,知道只需要创建一个新表。
10.2 添加基本书签视图
现在来添加两个视图来使用户可以添加和移除标签。在cab/views中创建一个文件bookmarks.py,然后开始编写add_bookmark视图:
1 from django.http import HttpResponseRedirect2 from django.shortcuts import get_object_or_404,render_to_response3 from django.contrib.auth.decorators import login_required4 from cab.models import Bookmark,Snippet5 6 def add_bookmark(request,snippet_id):7 snippet = get_object_or_404(Snippet,pk=snippet_id)8 try:9 Bookmark.objects.get(user__pk=request.user.id,
10 snippt__pk=snipet.id)
11 except Bookmark.DoesNotExist:
12 bookmark = Bookmark.objects.create(user-request.user,
13 snippet=snippet)
14 return HttpResponseRedirect(snippet.get_absolute_url())
15 add_bookmark = login_required(add_bookmark)
这里的逻辑十分简单。检查用户是否已经标记了这段代码,如果没有——这种情况下Bookmark.DoesNotExist异常将会被抛出——就新建一个书签。最后,返回一个到代码段的重定向,当然,还是要保证用户已经是登录。
删除一个书签也同样很简单:
1 def delete_bookmark(request,snippet_id):
2 if request.method == 'POST':
3 snippet = get_object_or_404(Snippet,pk=snippet_id)
4 Bookmark.objects.filter(user__pk=request.user.id,
5 snippet__pk=snippet.id).delete()
6 return HttpResponseRedirect(snippet.get_absolute_url())
7 else:
8 return render_to_response('cab/confirm_bookmark_delete.html',{'snippet':snippet})
9 delete_bookmark = login_required(delete_bookmark)
在delete_bookmark视图中,使用了两种重要的技术:
- 相对于先查询用户是否标记了这段代码,然后再删除它的方式(导致了两次数据库查询),这里简单的使用了过滤器filter()来创建了QuerySet来查找任何可能符合这个用户和这个代码段的书签。然后对这个QuertSet调用delete()方法。这个过程只需要一次查询——一个DELETE查询,而FROM子句会限制删除到正确的行上。
- 需要保证删除书签的操作使用的是HTTP POST。如果请求的方法不是POST,显示一个确认页面。
最后强调的一点,因为使用HTTP POST请求和对任何删除内容使用确认屏幕——甚至看起来很琐碎的内容像是书签——都是极为重要的习惯。不仅仅是因为阻止了用户因为点击了页面上错误的链接而导致意外删除,而且还稍微增强了对普通类型的网页攻击的安全上的措施:跨站请求伪装(CSRF)。在CSRF攻击中,黑客通过页面上隐藏的链接或者指向你应用的表单诱导你站点上的用户。
黑客利用了来自用户的HTTP请求,而许多应用是允许用户的HTTP请求来修改和删除内容的。
另外的,要求任何修改或者删除服务器端数据的操作都使用POST请求是一种好的实践。HTTP的说明中表明了确定的方法,包括GET方法,都应当被认为是安全且就基本上没有副作用的。
注意:安全和幂等的http方法
你已经编写的用于添加书签的视图可以通过HTTP GET来访问,看起来似乎和这一类型的视图应当具有的安全性相抵触。HTTP协议使用了两种不同但相关的概念来表述请求方法:安全的和幂等的。一个安全的请求没有副作用并简单的提取某些信息,而一个幂等的请求则请求一次和请求相同的多次都具有相同的副作用。HTTP要求GET请求是幂等的,但没有严格规定它们必须是安全的。add_bookmark视图是幂等的,因为同一个用户标记同一段代码的多次请求并不会创建多个Bookmark对象。实际的效果就像是只有一个请求一样,因为只有一个Bookmark对象最终生成。然而,add_bookmark视图在这种情况下是不安全的,因为它具有副作用(产生了一个Bookmark对象)。这并没有违反HTTP协议的规定,但是通常情况下,在允许GET请求据具有副作用的时候都要小心。在本例中,创建一个书签并不真的导致风险。例如,如果有人被诱导点击一个链接来对一段代码创建了书签,最坏的结果也不过就是需要再将书签删除掉。因此,基本上可以接受通过GET请求来创建书签。
确认页面的模板十分简单。你可以显示一些用户选择“解除标记"的代码段的信息,然后你可以包含一个简单的通过POST提交的确认表单:
1
2
3
注意:更多对CSRF的防护
一个HTTP POST有一些对CSRF的防护,因为这样能使攻击者几乎无法显示一个特定页面的链接并获得删除内容的触发器。但是,为了全面的保护,你需要引用Django内置的一个应用django.contrib.csrf,它提供了更强的功能。它会在一个POST提交的时候自动的插入并检查一个随机生成的字符串,如果该字符串不是从用户的浏览器上发送的,会返回一个HTTP 403(Forbidden)响应。
你可以在http://docs.djangoproject.com/en/dev/ref/conreib/csrf/找到关于这个系统的完整文档。
为URLs设置一个添加和删除书签也及其简单。你可以创建cab/urls/bookmarks.py然后写入:
1 from django.conf.urls.defaults import *2 from cab.views import bookmarks3 4 urlpatterns = patterns('',5 url(r'^add/(?P
10 name='cab_bookmark_delete'),
11 )
现在你已经有了管理书签的视图,继续编写一个显示当前用户书签列表的视图。这只需要封装通用视图object_list即可:
1 from django.views.generic.list_detail import object_list
2
3 def user_bookmarks(reuqest):
4 return object_list(query=Bookmark.objects.filter(user__pk=request.user.id),
5 teplate_nam='cab/user_bookmarks.html',
6 paginate_by=20)
你可以为视图设置一个URL,这样bookmark的根URLs会简单的显示用户的书签:
1 url(r'^$',bookmarks.user_bookmarks,name='cab_user_bookmarks'),
最后,来完善面向书签的视图,增加一个查询被标记最多的代码段功能。因为这个查询返回了Snippet对象,将其放入cab/managers.py中的SnippetManager:
1 def most_bookmarked(self):
2 return self.annotate(score=Count('bookmark')).order_by('score')
现在在cab/views/popular.py中编写most_bookmarked视图:
1 def most_bookmarked(reuqest):
2 return object_list(queryset=Snippet.objetcts.most_bookmarked(),
3 template_name='cab/most_bookmarked.html',
4 paginate_by=20)
然后添加URL模式到cab/urls/popular.py:
1 url(r'^bookmarks/$',popular.most_bookmarked,name='cab_most_bookmarked'),
10.3 创建一个新的模板标签{% if_bookmaeked %}
在使用add_bookmark和delete_bookmark视图的时候,你可能想要指示出当时候显示一个代码片段时用户是否已经用书签标记过。这样,你可以隐藏标记书签视图中的链接,或者选择显示一个链接或者按钮来删除标签。
你可以将其当成是代码段详细视图中的一部分来设置,但这也许不是唯一想要使用这种功能的地方。如果你显示一个代码段列表,例如,你可能想要一个快速和简单的方式来决定哪里显示书签标记链接,哪里则不显示。理想的解决办法是使用一个模板标签,可以表明一个用户是否已经标记了一段代码。理想的工作状态类似于:
注意:串联URLs
因为使用了{% url %}标签来生成到add_bookmark视图的链接,你需要将cab应用的URLs添加到项目的根URLconf模块(通过include()调用)。如果你使用了带有名字的{% url %}标签,但却没有在项目中设置它,那么就不会找到正确的URL,而只会简单的返回一个空字符串。
但是这个要怎么写呢?目前,所有你自定义的模板标签都十分简单。典型的情况就是它们读入参数然后返回一些东西给上下文(context)。编写这个标签需要两种新的技术:
- 编写一个标签可以在模板读取时先被找到,例如{% else %}子句及关闭标签,并对需要显示的内容加以追踪。
- 可以从模板上下文中解析出任意变量的能力,就像类似object这样的变量。
10.3.1 Django模板之前的解析
你可能还记得第六章中当编写第一个自定义模板标签的时候标签编译函数需要两个参数,传统上称作解析器(paser)和词法分析器(token)。当时,你只需要关心的是分析器(token)部分,因为它包含了你所关心的参数。但是,这次要的重点在于解析器——实际解析模板的对象——要排上用场了。
在深入之前,先来看看自定义标签的基础。在cab目录下,创建一个新的目录templatetags,在该目录中创建两个新文件:__init__.py和snippets.py。然后,打开cab/templatetags/snippets.py加入两个必须的导入语句:
1 from django import template
2 from cab.models import Bookmark
现在可以开始为{% if_bookmarked %}标签编写编译函数:
1 def do_if_bookmarked(parser,token):
2 bits = token.contents.split()
3 if len(bits) != 3
4 raise template.TemplateSyntaxError("%s tag takes two arguments" % bits[0])
这个编译函数查看调用标签时的语法——其形式为{% if_bookmarked user snippet %}——确定其有正确的参数数量,当参数数量错误时立即抛出TemplateSyntaxError。
现在可以将注意力转向paser参数,看看它有什么用处。你希望预先读取知道遇到{% else %}或者{% endif_bookmarked %}标签。你所要做的就是调用paser对象的paser()方法将你想要传递的东西作为一个列表传递给它。解析的结果会是一个django.template.NodeList类的实例,——顾名思义——一个模板节点的列表。
nodelist_true = paser.paser(('else','endif_bookmarked'))
将这个结果保存在了变量nodelist_true中,因为在if/else类型的标签行为中,它们对应了当条件为真时(即用户是否标记了代码段)想要显示的内容。
对parser.parse()的调用一直进行直到你告诉它要寻找的第一个项之前。这表示你想要查找下一个token并确定是否是一个{% else %}。如果是,就要做进一步的解析:
1 token = parser.next_token()
2 if token.contents == 'else':
3 nodelist_false = parser.parse(('endif_bookmarked'))
4 parser.delete_first_token()
5 else:
6 nodelist_false = template.NodeList()
如果解析器从你的列表中找到的第一个的确是{% else %}的话,那么就简单地创建一个空的NodeList。如果用户并没有书签标记代码段,那么当没有{% else %}子句的时候就不显示任何东西。
最后,返回一个Node类,将标签和两个NodeList实例整合在一起作为两个参数传递。尽管目前还没有定义它,将要使用的Node类被称为IfBookmarkedNode:
return IfBookmarkedNode(bits[1],bits[2],nodelist_true,nodelist-false)
10.3.2 解析模板Node中的变量
现在开始编写IfBookmarkedNode。显然,需要子类化template.Node,并且需要在它的__init__()方法中接收四个参数。两个NodeList实例会被简单的储存起来在渲染模板的时候使用:
1 class IfBookmarkedNode(template.Node):
2 def __init__(self,user,snippet,nodelist_true,nodelist_false):
3 self.nodelist_true = nodelist_true
4 self.nodelist_false = nodelist_false
但是user和snippet变量是什么呢?现在,它们是模板中取得的纯字符串,还不知道在查看上下文时它们会被解析成什么值。你需要声明它们是模板变量且在稍后解析。幸运的是,这很容易做到:
self.user = template.Variable(user)
self.snippet = template.Variable(snippet)
django.template中的Variable类替你完成了那些麻烦的任务。当模板上下文给出之后,它知道如何解析并给出实际对应的值。
现在你可以开始写render()方法:
1 def render(self,context):
2 user = self.user.resolve(context)
3 snippet = self.snippet.resolve(context)
每个Variable实例都有一个resolve方法,实际处理解析变量。如果得出的变量没有与任何东西对应,甚至还会处理异常——django.template.VariableDoesNotExist——自动的完成。当然,如你所见到的那样,通常在自定义的模板标签产生错误时进入安静模式是不错的主意,因此当有一个变量不可用时捕捉这个异常并使标签不返回任何东西:
1 def render(self,context):
2 try:
3 user = self.user.resolve(context)
4 snippet = self.snippet.resolve(context)
5 except template.VariableDoesNotExist:
6 return ''
如果通过了这个点,就可以知道变量解析成功完成了,可以使用它们查询一个存在的Bookmark了。现在唯一需要点技巧的事情就是确定每种不同情况下返回什么。现在有两个Nodelist实例,要根据用户是否书签标记了代码段来决定渲染哪一个。很简单,就像Node必须有一个render()方法来接收上下文并返回一个字符,Nodelist也是一样:
1 if Bookmark.objects.filter(user__pk=user.id,
2 snippet__pk=snippet.id):
3 return self.nodelist_true.render(context)
4 else:
5 return self.modelist_false.render(context)
现在你就有了一个完整的标签。注册之后,cab/templatetags/snippets.py看起来像这样:
1 from django import template2 from cab.models import Bookmark3 def do_if_bookmarked(parser, token):4 bits = token.contents.split()5 if len(bits) != 3:6 raise template.TemplateSyntaxError("%s tag takes two arguments" % bits[0])7 nodelist_true = parser.parse(('else', 'endif_bookmarked'))8 token = parser.next_token()9 if token.contents == 'else':
10 nodelist_false = parser.parse(('endif_bookmarked',))
11 parser.delete_first_token()
12 else:
13 nodelist_false = template.NodeList()
14 return IfBookmarkedNode(bits[1], bits[2], nodelist_true, nodelist_false)
15 class IfBookmarkedNode(template.Node):
16 def __init__(self, user, snippet, nodelist_true, nodelist_false):
17 self.nodelist_true = nodelist_true
18 self.nodelist_false = nodelist_false
19 self.user = template.Variable(user)
20 self.snippet = template.Variable(snippet)
21 def render(self, context):
22 try:
23 user = self.user.resolve(context)
24 snippet = self.snippet.resolve(context)
25 except template.VariableDoesNotExist:
26 return ''
27 if Bookmark.objects.filter(user__pk=user.id,
28 snippet__pk=snippet.id):
29 return self.nodelist_true.render(context)
30 else:
31 return self.nodelist_false.render(context)
32 register = template.Library()
33 register.tag('if_bookmarked', do_if_bookmarked)
现在可以在模板中简单的使用{% load snippet %}和{% if_bookmarked %}标签了。
10.4 使用RequestContext来自动填充模板变量
但是你只能在使用模板时有一个可用变量表示当前登录用户的前提下使用{% if_bookmarked %}标签。这是到目前为止需要一点技巧的主张,即你可以不编写视图来将当前用户作为模版变量传递。因为没有很大的必要做这件事。你已经通过访问reuqest.user在视图层面上做了全部有关登录用户的工作,所以到目前为止你还没有遇到那种真正需要一个表示模板中可用用户的情况。
现在你可以回过头去修改全部手写的视图,但是那样会立即带来两个缺陷:
a.冗繁和重复:基本上Django鼓励你避免任何可以被这样形容的事情。
b.对不是由你自己编写的视图来说毫无帮助:在许多情况下,你会简单的封装通用视图,几乎每次都需要通过手动向通用视图传递extra_context参数,看起来没有解决任何问题。另外,这种途径在你使用其他人编写的应用中的视图的时候没有任何帮助。如果别人写的视图不接受类似extra_context的参数,那你就不能做任何事。
幸运的是,有一个简单些的解决办法。回想一下第三章中第一个手写视图,传递给模板的变量和值的字典是一个django.template.Context实例。因为这是一个普通的Python类,你可以子类化并添加自定义行为。Django包含了一个非常有用的Context的子类——django.template.RequestContext——可以自动的每次填充一些额外的变量当不需要明确的指定那些变量或者在每个视图中定义它们的时候。
RequestContext由于使用了名为上下文处理器(context processors)的函数而得名。(在第六章中曾简单的提到过)。每个上下文处理器都是一个函数,它接收一个Django HttpRequest对象作为一个参数并根据这个HttpRequest返回一个变量。Requestcontext之后自动的将这些变量加入到上下文中,除了任何在运行视图函数过程中明确的传递给上下文的变量。
在通常的情况下,RequestContext读取TEMPLATE_CONTEXTX_PROCESSORS设置中的上下文处理器函数列表。默认设置中恰好包括了一个上下文处理器可以读取request.user来获得当前用户并将作为{{ user }}变量加入到上下文中。恰好这就是现在最需要东西。一旦一个视图使用了RequestContext,它的模板将可以使用{{ user }}变量并且与当前活动用户想相对应。
使用RequsetContext及其简单;只需要简单的导入:
1 from django.template import RequestContext
你可以在任何需要为模板使用上下文的地方使用它。普通Context和RequestContext的区别就是后者必须接受一个HttpRequest对象作为一个参数。例如,在视图中,你需要写成这样:
context = RequestContext(request,{'foo':bar})
它同样也可以使render_to_response()正常工作,尽管使用方法可能不同。例如,通常会写成这样:
return render_to_response('example.html',{'foo':bar})
现在需要写成这样:
return render_to_response('example.html',{'foo':bar},context_instance=RequestContext(request))
注意:重复使用Requestcontext
尽管RequestContext明显的使处理在模版中可用的全局变量时的工作简单了不少,但是每次使用时都手动的输入任然显得有点重复劳动。如果通用视图可以自动的使用RequestContext,那么为什么不然快捷方式例如render_to_resopnse()也使用它呢?实际上,为什么不就把它当成是默认的context类呢?
一个原因是,RequestContext要求访问HttpRequest对象,而这是没有办法自动访问的。除非HttpRquest被显式的传递,否则RequestContext不能做任何事情。另一个好的理由是,在大量的情况下,你想要独立于HTTP请求进程而渲染一个模版。对Django模版系统来说,生成emial信息,将文件写入硬盘,及其他一些选项几很少直接使用到HTTP请求响应循环。
但是,如果你发现急切的需要一个快捷方式,你可以简单的写成:
from django.shortcuts import render_to_resopnse
from django.template import RequestContext
def render_response(request,*args,**kwargs):
kwagrs['context_instance'] = RequestContext(request)
return render_to_response(*args,**kwargs)
从个人观点来说,我建议不要这么做,从编码风格来说,我建议在每次使用RequestContext的时候简单的显式的写出来。我发现这样做能够提醒自己设置了一个额外的变量由RequestContext来填充的视图。另外,这点额外的代码,能使它在之后重新阅读视图函数代码的时候很容易被认出来。手动的处理RequestContext还能避免编码时过于依赖那些不随特定应用发布的快捷函数,这样就增加了代码的可重用性。
10.5 添加用户评分系统
唯一剩下的需要实现的特性就是一个评分系统,可以允许用户标记发现的最有用的代码段(也可以是没用的,genuine情况而定)。再一次的,还是以一个数据模型开始。就像书签系统那样,相当简单。你需要收集四条信息:
- 被评分的代码段
- 评分的用户
- 评价的分值,在本例中是-1或+1,适用于"顶或踩"的评分系统
- 评分日期
很容易在cab/models.py中构建一个这样的Rating模型:
1 class Rating(models.Model):2 RATING_UP = 13 RATING_DOWN = -14 RATING_CHOICES = ((RATING_UP,'useful')5 (RATING_DOWN,'not useful'))6 snippet = models.ForeignKey(Snippet)7 user = models.ForeignKey(User,related_name='cab_rating')8 date = models.DateTimeField()9
10 def __unicode__(self):
11 return "%s rating %s (%s)" % (self.user,self.snippet,self.get_rating_display())
12
13 def save(self):
14 if not self.id:
15 self.date = datetime.datetime.now()
16 super(Rating,self).save()
就像在Bookmark模型中一样,你明确设置了与User模型相关联的related_name以防止与其它的可能定义了评分系统的应用发生潜在的命名冲突。同时,评分值使用了一个整数字段,并付给合适的常量名,来处理实际的“顶”和“踩”的评价分值,和weblog应用中Entry模型的status字段类似。但是有一项稍微不同:在__unicode__()方法中,调用了一个方法名为get_rating _display()。任何时候一个模型拥有一个类似带有选项的字段,Django会自动的添加一个方法——名字由派生而来的字段名决定——会为当前选择的值返回一个可读的名字。
在cab/models.py中,同样也可以为Snippet模型增加一个方法,通过汇总所有评分来用于计算一个snippet的分值。这个方法将使用Django的汇总支持,但是使用了一个不同类型的汇总过滤器:django.db.models.Sum。这个过滤器,就像它的名字标明的,累加数据库中一系列的值并返回一个总和。
你也可以使用一个不同的方法来进行汇总。之前使用过annotate方法,因为需要向查询返回的结果加入一个额外的信息。但是现在你只想直接返回汇总值,因此使用一个不同的汇总方法称为aggregate。如果有一个Snippet对象保存在名为snippet的变量中,需要汇总相关的评分,你可以像这样编写查询:
1 from django.db.models import Sum
2 total_rating = snippet.rating_set.aggregate(Sum('rating'))
还可以在Snippet模型中用get_score方法增添这个功能(记住在models.py文件中加入导入Sum的语句):
def get_score(self):return self.rating_set.aggregate(Sum('rating'))
最后,在cab/managers.py文件中,还可以在SnippetManager中加入一个方法来计算排位最高的代码段(记住加入导入Sum的导入语句):
def top_ratede(self):return self.annotate(score=Sum('rating')).order_by('score')
这里处理了所有你需要的自定义查询,接下来运行manage.py syncdb来安装Rating模块。
10.5.1 为Snippet评分
让用户能够为代码段评分十分简单。你所需要的就是一个可以获取snippet ID和“顶”或“踩”评分的视图,然后添加一个Rating对象。视图逻辑相当简单。再创建一个视图文件——cab/views/rating.py——加入如下代码:
1 from django.http import HttpResponseRedirect2 from django.shortcuts import get_object_or_4043 from django.contrib.auth.decorators import login_required4 from cab.models import Rating,Snippet5 6 def rate(request,snippet_id);7 snippet = get_object_or_404(Snippet,pk=snippet_id)8 if 'rating' not in request.GET or reuqest.GET['rating'] not in ('1','-1'):9 return HttpResponseRedirect(snippet.get_absolute_url())
10 try:
11 rating = Rating.objects.get(user__pk=request.user.id,snippet_pk=snippet.id)
12 except Rating.DoesNotExist:
13 rating = Rating(user=request.user,snippet=snippet)
14 rating.rating = int(request.GET['rating'])
15 rating.save()
16 return HttpResponseRedirect(snippet.get_absolute_url())
17 rate = login_required(rate)
这里有两个技巧:
- 你可能希望这个视图接收一个像?rating=1或者?rating=-1的查询串,所以需要确定这个字符串且带有一个可以接受的值。否则,只需要重定位至代码段。
- 为了避免投票系统被用户用来对一个代码段不断的评分,需要确保视图在代码段已经存在评分的情况下改变评分可选项。
为视图设置URL相当简单。你可以简单添加cab/urls/rating.py文件然后设置所需的URL模式:
1 from django.conf.urls.defaults import *
2 from cab.veiws.ratings import rate
3
4 urlpatterns = patterns('',url(r'^(?P
10.5.2添加一个{% if_rated %}模板标签
接下来继续添加一个{% if_rated %}模板标签,类似于这一章之前完成的{% if_bookmarked %}标签。相应的编译函数看起来也很类似:
1 def do_if_rated(parse,token):2 bits = token.contents.split()3 if len(bits) != 3:4 raise template.TemplateSyntaxError("%s tag takes two arguments" % bits[0])5 nodelist_true = parser.parse(('else','endif_rated'))6 token = parser.next_token()7 if token,contents =='else':8 nodelist_false = parser.parse(('endif_rated'))9 parser.delete_first_token()
10 else:
11 nodelist_false = template.NodeList()
12 return IfRatedNode(bits[1],bits[2],nodelist_true,nodelist_false)
再一次,对模板使用了预解析并根据标签确定了可能的if/else结构,并保存了两个NodeList实例作为参数传递到Node类,叫做IfRatedNode。首先,你需要在文件开始将导入语句从:
from cab.models import Bookmark
修改为
from cab.models import Bookmar,Rating
然后编写IfRatedNode类:
1 class IfRatedNode(template.Node):2 def __init__(self,user,snippet,nodelist_true,nodelist_false);3 self.nodelist_true = nodelist_true4 self.modelist_false = nodelist_false5 self.user = template.Variable(user)6 self.snippet = template.Variable(snippet)7 8 def render(self,context);9 try:
10 user = self.user.resolve(context)
11 snippet = self.snippetresolve(context)
12 except template.VariableDoesNotExist:
13 return ''
14 if Rating.objects.filter(user__pk=user.id,snippet__pk=snippet.id):
15 return self.nodelist_true.render(context)
16 else:
17 return self.nodelist_false.render(context)
在文件底部注册标签:
register.tag('if_rated',do_if_rated)
10.5.3 提取一个用户的评分
现在已经有了{% if_rated %}标签,你可以添加第二个补充标签来提取用户对特定代码段的评分。这个新的{% get_rating %}标签允许你设置一个这样的模板:
{% load snippets %}
{% if_rated user snippet %}
{% get_raitng user snippet as rating %}
You rated this snippet {{ rating.get_rating_display }}.
{% endif_rated %}
当用户有一个已经评分的代码段,这段代码会像这样显示,“You rated this snippet useful.”
这个新标签的编译函数,do_get_rating,简单明了:
1 def do_get_rating(parser,token):
2 bits = token.contents.split()
3 if len(bits) != 5:
4 raise template.TemplateSyntaxError("%s tag takes four arguments" % bits[0])
5 if bits[3] != 'as':
6 raise template.TemplateSyntaxError("Third argumnet to %s must be 'as'" % bits[0])
7 return GetRatingNode(bits[1],bits[2],bits[4])
这个Node类,叫做GetRatingNode,同样很容易编写。只需要解析user和snippet变量,提取Rating,将其放入上下文中:
1 class GetRatingNode(template.Node):2 def __init__(self,user,snippet,varname):3 self.user = template.Variable(user)4 self.snippet = template.Variable(snippet)5 self.varname = varname6 7 def render(self,context):8 try:9 user = self.user.resolve(context)
10 snippet = self.snippet.resolve(context)
11 except template.VariableDoesNotExist:
12 return ''
13 rating = Rating.objects.get(user_pk=user.id,snippet__pk=snippet.id)
14
15 context[self.varname] = rating
16 return ''
接下来,注册标签:
register.tag('get_rating',do_get_rating)
然后,这样使用标签(举例,在显示详细snippet的页面中):
1 {% load snippets %}
2 {% if_rated user object %}
3 {% get_rating user snippet as rating %}
4
You rated this snippet{{ rating.get_rating_display }}.
5 {% else %}
6
Rate this snippet:
7 useful or
8 not useful.
9 {% endif_rated %}
10.6 展望
现在,你已经实现了代码分享应用特性列表上的全部内容。用户可以提交和编辑代码段,标记它们,根据语言来分类它们。还可以标记和评分,还拥有汇总视图来显示诸如评分最高和被标记最多的代码段以及使用最多的语言这样的功能。通过这些实践,你已经学习了怎样使用Django的表单系统,也了解了一些使用对象关系映射(ORM)和模板引擎的高级技巧。
当然,你任然可以加入更多的特性:
- 根据你在weblog应用中学到的经验,你可以容易的添加评论(with moderation)和feeds源。
- 你可以借用在weblog中编写的提取内容的模板标签,来提取最近的代码段或者修改为适用于所编写应用的进行某种自定义查询的功能。
- 还可以建立大量的视图和查询;即使是对这里设置的简单模型,也有很大的以有趣的方式拓展这个应用的空间,目前所设置的只是一个表面上的草图。
- 你可以考虑将这个应用与其他已经编写和使用的应用进行整合(也许可以是一个代码分享站点附带的博客上显示网站工作人员喜欢的代码段)。
现在,已经到了可以自行编写这些特性并精确的控制应用,按自己的方式来剪裁它。考虑一些这样的设想,并思考如何实现它们,然后坐下来编写它们。然后进行一番头脑风暴,想想那些没有在执行类表中的东西,尝试着动手实现它们。因为如果已经进行到了这里,你已经具有了运用你的知识来让Django为你所用的能力。
按照上述,我不会再给你任何关于特性列表或者需要实现什么的指示。相反,在之后的两章中,我会转换频道来讲一些在开发Django应用中基本上是最好的实践并且最有效的利用它们。