标签tag在很多web2.0应用中都很常见,标签其实就是关联某些信息的一个关键字。打标签实际上就是给内容分配标签的过程,它通常由作者或者用户实现。标签之所有这么流行是因为它允许用户对自己创建的博客、图像、链接等等进行分类。
既然我们要创建的是社交型网络收藏夹,标签自然非常重要,为了引进标签,我们需要一个机制来允许用户在提交bookmarks的时候带上标签一起存进数据库,并且可以浏览某个标签下的所有bookmarks。
Tag 数据模型
标签需要保存在数据库中,并且与bookmarks相关联,所以引进标签的第一步就是为了标签创建数据模型,标签对象的属性包括标签名,由于tag与bookmakrs之间的关系是多对多的关系,所以还有一个多对多字段类型的bookmarks。
编辑bookmarks/models.py,添加如下代码:
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 bookmarks
输出如下(红色部分):
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_bookmarks" ( "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可以放在相关联的数据模型中的任意一个,也就是说我们可以将models.ManyToManyField放在Bookmark中也可以放在Tag中,但是我们后面才创建Tag,所以就放在Tag里面。
打开交互命令窗口:
$ python manage.py shell >>> from bookmarks.models import * >>> bookmark = Bookmark.objects.get(id=1) >>> bookmark.link.url ‘http://www.packtpub.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>]
现在创建了两个tag,并将它们分配格bookmark,尽管我们没有修改Bookmark数据模型,创建Tag模型之后,Django自动给Bookmark增加一个属性tag_set,那么如何获取属于某个标签的bookmark呢?这个可以通过我们之前定义的Tag对象bookmark属性获取。
>>> tag1.bookmarks.all() [<Bookmark: Bookmark object>]
当我们通过交互命令行窗口print标签对象时,返回的是个很模糊的字符串,无法正确表达tag的内容,可以通过给Tag类增加__str__方法来打印友好信息。
编辑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对象自身已经定义了__str__方法。
创建Bookmark提交表单
接下来创建bookmark提交表单,这个表单允许用户输入url,用户名,以及标签名,创建这个表单的过程与之前创建注册表单的一样。
第一步,定义BookmarkSaveForm类,编辑bookmarks/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=‘Tags‘, required=False, widget=forms.TextInput(attrs={‘size‘: 64}) )
给每个字段都定义了label名以及表单元素类型,通过attrs参数设置文本框的输入长度限制。
通过在表单中指定正确的字段类型,我们不需要再提供其他的输入验证,例如,Django会自动确保用户输入了正确的URL,因为相对应的字段是models.URLField类型。
用户可以tag文本框中输入多个标签,标签之间以空格分开。
接着再创建视图函数,一样的,这个视图函数与注册表单的视图函数非常类似,在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.cleaned_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.cleaned_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)
这个视图函数首先判断请求方法是POST还是GET,如果是POST则提交表单进行处理,并重定向至用户页面,如果是GET方法,则返回注册表单页面。为了获取Link对象,这里我们使用了Link.objects.get_or_create方法,这个方法会根据参数尝试获取对应的Link对象,如果没有查询到,则创建一个新的Link对象并保存到数据库中,它返回两个值,一个是查询到的获取新建的Link对象,还有一个布尔值flag,如果是新建的则返回True,如果是查询到的则返回False。
创建模板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 }} {% csrf_token %} <input type="submit" value="save" /> </form> {% endblock %}
最后添加url,编辑urls.py:
(r‘^save/$‘, bookmark_save_page),
收工,登录之后,导航至http://127.0.0.1:8000/save/,就可以看到bookmark提交表单了。
最后还得给导航菜单添加创建bookmark的链接并且限制只有已登录才能访问这个页面。
限制只允许已登陆用户访问
在导航菜单中添加创建bookmark的链接,然后稍微重构以下HTML代码,打开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>
注意到没有我们必须确保提交bookmark表单之前用户必须已经登录,这是因bookmark与User对象存在关联,同时我们也不允许匿名用户保存bookmark。那么我们如何确保匿名用户不能提交表单呢?实际上有很多方法,这里我介绍两种。
在上一节中我们看到当前用户是否已经登录可以通过request.user.is_authenticated()来判断,所以我们可以使用一个if语句来包裹bookmark_save_page视图函数,如下所示:
if request.user.is_authenticated(): # Process form. else: # Redirect to log-in page.
由于限制页面访问的情景使用这么多,所以Django提供了一个快捷方式-修饰器login_required:
from django.contrib.auth.decorators import login_required @login_required def bookmark_save_page(request):
这样我们就可以将视图函数限制为只给已经登录的用户调用,我们只需要从django.contrib.auth.decorators中导入login_required这个修饰器。Python中修饰器其实就是一个能够对其他函数进行修改的函数。这个修饰器在调用视图函数之前先检查用户是否已经登录。
有一个地方需要注意,login_required是如何知道我们登录时使用的url呢?默认的URL为/accounts/login。如果需要进行修改,我们可以将登录的URL保存在变量LOGIN_URL中,这个变量位于django.contrib.auth中,所以可以在settings.py中添加如下代码:
import django.contrib.auth django.contrib.auth.LOGIN_URL = ‘/login/‘
除此之外,还可以给login_required提供一个参数login_url="/login/"。
现在注销用户之后,再点击链接至http://127.0.0.1:8000/save/,你就会自动重定向至登录界面。
浏览bookmarks的方法
浏览bookmarks是我们找这个应用的核心功能,因此,为用户提供浏览以及分享bookmarks功能非常重要。
尽管有多种方法浏览bookmarks,但是生成bookmarks列表的机制是一样的。
- 首先使用Django数据模型创建一个bookmarks组成的列表。
- 其次,使用模板渲染bookmarks列表。
尽管不同页面生成bookmarks列表的细节不太一样,但是所有的bookmarks列表看起来都是相似的,使用链接展示bookmark,下面是一个tag以及用户信息。如果将这个通用的部分提炼出来,然后在其他页面中重用,将节省很多代码。Django模板系统提供了include标签支持这个功能。
include标签的作用很明显,就是在一个模板中包含另外一个模板,相当于复制模板的内容给另一个。让我们也创建一个通用的模板,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信息,否则输出没有bookmarks的信息。在for循环中输出bookmark链接以及tag信息,最后输出用户信息。
改进用户页面
为了使用上面创建的bookmark_list.html,需要再user_page.html中包含这个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 %}
这样一来,模板就变得好简单了,这样做的好处就是我们可以重用bookmark_list.html。
在用户页面中只需要显示tag信息,而不需要显示用户信息,所以需要传递show_tag=True给模板,编辑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)
这里我们使用了另外一个快捷方法,get_object_or_404,它首先尝试获取这个对象,如果成功就返回,如果失败,则抛出404异常。
此外,我们还修改了bookmark列表获取的方式,这里使用了order_by方法而不是all方法,oder_by根据给定的参数选择降序排列还是升序排列,如果参数前面为-,则为降序排列。
为了改善tags的外观,编辑/static/style.css:
ul.tags, ul.tags li { display: inline; margin: 0; padding: 0; }
这样一来,将tag列表展现在同一行,看起来更美观,也更节省空间。
创建Tag页面
接下来创建Tag页面,实际上并不需要编写太多新的代码,只需要将之前的代码进行一些整合即可。
首先打开urls.py,添加如下url:
(r‘^tag/([^\s]+)/$‘, tag_page),
这里使用的正则表达式与用户页面中url使用的不太一样,因为tag名只要不包含空白字符即可。而用户名只能包含字母以及数字。
接下来创建tag_view视图,编辑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)
这个代码的结构与user_page视图相似。
最后创建模板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 %}
实际上这个模板的内容与user_page.html的一模一样,现在我们知道代码重用的方便性了吧。
最后一步,给bookmark列表中的tag添加到各种tag页面的链接,编辑bookmark_list.html,修改生成tag列表的代码部分:
<ul class="tags"> {% for tag in bookmark.tag_set.all %} <li> <a href="/tag/{{ tag.name }}/">{{ tag.name }}</a> </li> {% endfor %} </ul>
这个改动也会自动应用到用户以及tag页面。
创建标签云
‘ 标签云是展示系统中所有标签的一个形象化表示。它除了展示所有标签之外,还可以展示每个标签使用的频率。标签名的大小代表它被使用的频率。字体越大,说明使用的频率越高。
实现标签云的关键是创建一个tags列表,同时这个tag对象还包含一个count属性,代表它被使用的次数,然后找到所有标签中count的最大值与最小值,然后根据这两个值求出每个标签的权重,根据这个值来显示调整字体的大小。
编辑bookmarks/views.py,创建tag_cloud_page视图:
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)
接下来创建模板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 %}
然后添加样式,编辑/static/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://localhost:8000/tag/看看最终效果。
安全考虑
在这一节的开头,我们设计的bookmark表单可以接收用户输入并保存到数据库中,最后将它显示给游客。由于我们的应用是对外开放的,所以任何人都可以注册并提交数据,即使某些数据是有恶意的,所以我们必须采取预防措施,阻止恶意数据的提交。
Web开发得黄金准则就是绝对不要相信用户的输入。你必须在将数据保存到数据库之前或者显示给用户之前对它们进行验证,这一节,我们将讨论如何实现这个目的,以及如何避免web应用中常见的两个安全性问题。
SQL注入
在针对web应用的攻击中最常见的就是SQL注入了,在这种攻击中,攻击者使用特定的技巧或者通过操纵SQL查询语句来获取关键数据或者将恶意的数据保存在数据库中。SQL注入容易发生在开发者不对用户输入进行转义就拼接到SQL语句中的时候。因为我们使用Django进行查询或者保存数据,所以我们对这种攻击是免疫的,因为数据模型的API会自动对用户输入进行转义,所以我们也不需要再做其他的预防措施。
跨站脚本攻击
另外一种常见的攻击就是跨站脚本攻击,这种情况下,攻击者在输入中嵌入Javascript恶意代码,当这个代码渲染到HTML中时,Javascript代码会获取页面的控制权,然后就可以获取cookie等隐私信息。为了预防这个攻击,用户的输入都应该在提交之后进行转义,Django不会自动为我们处理这种情况,但是它提供了相应的方法。
让我们在自己的项目中实践,bookmark表单中的title允许用户输入任何信息。例如,可以输入下面的标题名:
<script>alert("Test.");</script>
这段Javascript代码只是简单的弹出一个警告框,但是我们的应用会将它作为bookmark的标题名并保存在数据库中,数据模型在将它保存在数据库之前会对它进行转义,但是用户打开一个包含这个bookmark的页面时,将执行这段代码。
这段代码不会有任意的伤害,但是在真实的世界里,攻击者可以通过这个方法获取隐私信息。
幸运的是,这个问题的解决办法很简单,Django提供了一种模板过滤的机制,可以对变量进行修改,其中一种过滤器就是escape,它可以对变量进行转义,另外一个就是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 %} [...]
使用过滤器的语法与shell中的管道命令类似,模板过滤器实际上是个Python函数,当过滤器作用到变量上时,这个变量就传递给过滤器函数作为参数,然后返回修改之后的结果。
escape实际上就是将<、>等转义成<,>。
同样的,给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 %}
用户名不需要进行转义,因为它的组成只能是数字以及字母。