• 三步创建博客应用
    • Tasking
    • 创建BlogpostAPP
      • 生成APP
      • 创建Model
      • 配置URL
    • 创建View
      • 创建博客列表页
      • 创建博客详情页
    • 测试
      • 测试首页
      • 测试详情页

    三步创建博客应用

    Tasking

    在我们不了解Django的时候,要对这样一个任务进行Tasking,有点困难。不过,我们还是可以简单地看看是应该如何去做:

    • 生成APP。对于大部分主流的Web框架来说,它们都可以手动地生成一些脚手架,如Ruby语言中的Ruby On Rails、Node.js中的Express等等。
    • 创建对应的Model,即其在数据库中存储的模型与我们在代码中要使用的模型。
    • 创建程序对应的View,用于处理数据。
    • 创建程序的Template,用于显示数据。
    • 编写测试来保证功能。

    对于其他应用来说也是差不多的。

    创建BlogpostAPP

    生成APP

    现在我们可以开始创建我们的APP,使用下面的代码来创建:

    $ django-admin startapp blogpost

    会在blogpost目录下,生成下面的文件:

    1. .
    2. ├── __init__.py
    3. ├── admin.py
    4. ├── apps.py
    5. ├── migrations
    6. └── __init__.py
    7. ├── models.py
    8. ├── tests.py
    9. └── views.py

    创建Model

    现在,我们需要来创建博客的Model即可。对于一篇基本的博客来说,它会包含下面的几部分内容:

    • 标题
    • 作者
    • 链接(中文更需要一个好的链接)
    • 内容
    • 发布日期

    我们就可以按照上面的内容来创建我们的Blogpost model:

    1. from django.db import models
    2. from django.db.models import permalink
    3. class Blogpost(models.Model):
    4. title = models.CharField(max_length=100, unique=True)
    5. author = models.CharField(max_length=100, unique=True)
    6. slug = models.SlugField(max_length=100, unique=True)
    7. body = models.TextField()
    8. posted = models.DateField(db_index=True, auto_now_add=True)
    9. def __unicode__(self):
    10. return '%s' % self.title
    11. @permalink
    12. def get_absolute_url(self):
    13. return ('view_blog_post', None, { 'slug': self.slug })

    上面的get_absolute_url方法就是用于返回博客的链接。之所以使用手动而不是自动生成,是因为自动生成不靠谱,而且不利

    然后在Admin注册这个Model

    1. from django.contrib import admin
    2. from blogpost.models import Blogpost
    3. class BlogpostAdmin(admin.ModelAdmin):
    4. exclude = ['posted']
    5. prepopulated_fields = {'slug': ('title',)}
    6. admin.site.register(Blogpost, BlogpostAdmin)

    接着我们需要先将blogpost这个APP添加到配置文件blog/blog/settings.pyINSTALLED_APPS字段中:

    1. INSTALLED_APPS = [
    2. 'blogpost.apps.BlogpostConfig',
    3. 'django.contrib.admin',
    4. ...
    5. ]

    然后做数据库迁移:

    1. python manage.py migrate

    这时会提示:

    1. Operations to perform:
    2. Apply all migrations: admin, contenttypes, auth, sessions
    3. Running migrations:
    4. No migrations to apply.
    5. Your models have changes that are not yet reflected in a migration, and so won't be applied.
    6. Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.

    是因为我们忘记了先运行

    1. python manage.py makemigrations

    进入后台,我们就可以看到BLOGPOST的一栏里,就可以对其进行相关的操作。

    Django后台界面

    点击Blogpost的Add后,我们就会进入如下的添加博客界面:

    Django添加博客

    实际上,这样做的意义是将删除(Delete)、修改(Update)、添加(Create)这些内容交给用户后台来做,当然它也不需要在View/Template层来做。在我们的Template层中,我们只需要关心如何来显示这些数据。

    现在,我们可以执行一次新的代码提交——因为现在的代码可以正常工作。这样出现问题时,我们就可以即时的返回上一版本的代码。

    1. git add .
    2. git commit -m "create blogpost model"

    然后再进行下一步地操作。

    配置URL

    现在,我们就可以在我们的urls.py里添加相应的route来访问页面,代码如下所示:

    1. from django.conf import settings
    2. from django.conf.urls import include, url
    3. from django.conf.urls.static import static
    4. from django.contrib import admin
    5. urlpatterns = [
    6. (r'^$', 'blogpost.views.index'),
    7. url(r'^blog/(?P<slug>[^\.]+).html', 'blogpost.views.view_post', name='view_blog_post'),
    8. url(r'^admin/', include(admin.site.urls))
    9. ]

    在上面的代码里,我们创建了两个route:

    • 指向首页,其view是index
    • 指向博客详情页,其view是view_post

    指向博客详情页的URL正则r'^blog/(?P<slug>[^\.]+).html,会将形如blog/hello-world.html中的hello-world提取出来作为参数传给view_post方法。

    接着,我们就可以创建两个view。

    创建View

    创建博客列表页

    对于我们的首页来说,我们可以简单的只显示五篇博客,所以我们所需要做的就是从我们的Blogpost对象中,取出前五个结果即可。代码如下所示:

    1. from django.shortcuts import render, render_to_response, get_object_or_404
    2. from blogpost.models import Blogpost
    3. def index(request):
    4. return render_to_response('index.html', {
    5. 'posts': Blogpost.objects.all()[:5]
    6. })

    Django的render_to_response方法可以根据一个给定的上下文字典渲染一个给定的目标,并返回渲染后的HttpResponse。即将相应的值,如这里的Blogpost.objects.all()[:5],填入相应的index.html中,再返回最后的结果。

    首先,我们需要创建一个templates文件夹,然后在setting.py的TEMPLATES字段将该目录指定为默认目录

    1. TEMPLATES = [
    2. {
    3. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
    4. 'DIRS': ['templates/'],
    5. 'APP_DIRS': True,
    6. 'OPTIONS': {
    7. 'context_processors': [
    8. 'django.template.context_processors.debug',
    9. 'django.template.context_processors.request',
    10. 'django.contrib.auth.context_processors.auth',
    11. 'django.contrib.messages.context_processors.messages',
    12. ],
    13. },
    14. },
    15. ]

    另外,在templates目录下我们需要新建base.html, index.html和blogpost_detail.html三个模板。

    1. {% load staticfiles %}
    2. <html>
    3. <head>
    4. <meta charset="utf-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1">
    7. <title>{% block head_title %}Welcome to my blog{% endblock %}</title>
    8. <link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.min.css' %}">
    9. <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
    10. </head>
    11. <body data-twttr-rendered="true" class="bs-docs-home">
    12. <header class="navbar navbar-static-top bs-docs-nav" id="top" role="banner">
    13. <div class="container">
    14. <div class="navbar-header">
    15. <button class="navbar-toggle collapsed" type="button" data-toggle="collapse"
    16. data-target=".bs-navbar-collapse">
    17. <span class="sr-only">切换视图</span>
    18. <span class="icon-bar"></span>
    19. <span class="icon-bar"></span>
    20. <span class="icon-bar"></span>
    21. </button>
    22. <a href="/" class="navbar-brand">Growth博客</a>
    23. </div>
    24. <nav class="collapse navbar-collapse bs-navbar-collapse" role="navigation">
    25. <ul class="nav navbar-nav">
    26. <li>
    27. <a href="/pages/about/">关于我</a>
    28. </li>
    29. <li>
    30. <a href="/pages/resume/">简历</a>
    31. </li>
    32. </ul>
    33. <ul class="nav navbar-nav navbar-right">
    34. <li><a href="/admin" id="loginLink">登入</a></li>
    35. </ul>
    36. <div class="col-sm-3 col-md-3 pull-right">
    37. <form class="navbar-form" role="search">
    38. <div class="input-group">
    39. <input type="text" id="typeahead-input" class="form-control" placeholder="Search" name="search" data-provide="typeahead">
    40. <div class="input-group-btn">
    41. <button class="btn btn-default search-button" type="submit"><i class="glyphicon glyphicon-search"></i></button>
    42. </div>
    43. </div>
    44. </form>
    45. </div>
    46. </nav>
    47. </div>
    48. </header>
    49. <main class="bs-docs-masthead" id="content" role="main">
    50. <div class="container">
    51. <div id="carbonads-container">
    52. THE ONLY FAIR IS NOT FAIR <br>
    53. ENJOY CREATE & SHARE
    54. </div>
    55. </div>
    56. </main>
    57. <div class="container" id="container">
    58. {% block content %}
    59. {% endblock %}
    60. </div>
    61. <footer class="footer">
    62. <div class="container">
    63. <p class="text-muted">@Copyright Phodal.com</p>
    64. </div>
    65. </footer>
    66. <script src="{% static 'js/jquery.min.js' %}"></script>
    67. <script src="{% static 'js/bootstrap.min.js' %}"></script>
    68. <script src="{% static 'js/bootstrap3-typeahead.min.js' %}"></script>
    69. <script src="{% static 'js/main.js' %}"></script>
    70. </body>
    71. </html>

    在我们的index.html中,我们就可以拿到前五篇博客。我们只需要遍历出posts,拿出每个post相应的值,就可以完成列表页。

    1. {% extends 'base.html' %}
    2. {% block title %}Welcome to my blog{% endblock %}
    3. {% block content %}
    4. <h1>Posts</h1>
    5. {% for post in posts %}
    6. <h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
    7. <p>{{post.posted}} - By {{post.author}}</p>
    8. <p>{{post.body}}</p>
    9. {% endfor %}
    10. {% endblock %}

    在上面的模板里,我们还取出了博客的链接用于跳转到详情页。

    创建博客详情页

    依据上面拿到的slug,我们就可以创建对应的详情页的view,代码如下所示:

    1. def view_post(request, slug):
    2. return render_to_response('blogpost_detail.html', {
    3. 'post': get_object_or_404(Blogpost, slug=slug)
    4. })

    这里的get_object_or_404将会根据slug来获取相应的博客,如果取不出相应的博客就会返回404。因此,我们的详情页和上面的列表页也是类似的。

    1. {% extends 'base.html' %}
    2. {% block head_title %}{{ post.title }}{% endblock %}
    3. {% block title %}{{ post.title }}{% endblock %}
    4. {% block content %}
    5. <h2>{{ post.title }}</a></h2>
    6. <p>{{post.posted}} - By {{post.author}}</p>
    7. <p>{{post.body}}</p>
    8. {% endblock %}

    随后,我们就可以再提交一次代码了。

    测试

    TDD虽然是一个非常好的实践,但是那是对于那些已经习惯写测试的人来说。如果你写测试的经历非常少,那么我们就可以从写测试开始。

    在这里我们使用的是Django这个第三方框架来完成我们的工作,所以我们并不对这个框架的功能进行测试。虽然有些时候正是因为这些第三方框架的问题而导致的Bug,但是我们仅仅只是使用一些基础的功能。这些基础的功能也已经在他们的框架中测试过了。

    测试首页

    先来做一个简单的测试,即测试我们访问首页的时候,调用的函数是上面的index函数

    1. from django.core.urlresolvers import resolve
    2. from django.http import HttpRequest
    3. from django.test import TestCase
    4. from blogpost.views import index, view_post
    5. class HomePageTest(TestCase):
    6. def test_root_url_resolves_to_home_page_view(self):
    7. found = resolve('/')
    8. self.assertEqual(found.func, index)

    但是这样的测试看上去没有多大意义,不过它可以保证我们的route可以和我们的URL对应上。在编写完测试后,我们就可以命令提示行中运行:

    1. python manage.py test

    来查看测试的结果:

    1. Creating test database for alias 'default'...
    2. .
    3. ----------------------------------------------------------------------
    4. Ran 1 test in 0.031s
    5. OK
    6. Destroying test database for alias 'default'...
    7. (growth-django)

    运行通过,现在我们可以进行下一个测试了——我们可以测试页面的标题是不是我们想要的结果:

    1. def test_home_page_returns_correct_html(self):
    2. request = HttpRequest()
    3. response = index(request)
    4. self.assertIn(b'<title>Welcome to my blog</title>', response.content)

    这里我们需要去请求相应的页面来获取页面的标题,并用assertIn方法来断言返回的首页的html中含有<title>Welcome to my blog</title>

    测试详情页

    同样的我们也可以用测试是否调用某个函数的方法,来看博客的详情页的route是否正确?

    1. class BlogpostTest(TestCase):
    2. def test_blogpost_url_resolves_to_blog_post_view(self):
    3. found = resolve('/blog/this_is_a_test.html')
    4. self.assertEqual(found.func, view_post)

    与上面测试首页不一样的是,在我们的Blogpost测试中,我们需要创建数据,以确保这个流程是没有问题的。因此我们需要用Blogpost.objects.create方法来创建一个数据,然后访问相应的页面来看是否正确。

    1. def test_blogpost_create_with_view(self):
    2. Blogpost.objects.create(title='hello', author='admin', slug='this_is_a_test', body='This is a blog',
    3. posted=datetime.now)
    4. response = self.client.get('/blog/this_is_a_test.html')
    5. self.assertIn(b'This is a blog', response.content)

    或许你会疑惑这个数据会不会被注入到数据库中,请看运行测试时返回的结果的第一句:

    1. Creating test database for alias 'default'...

    Django将会创建一个数据库用于测试。

    同理,我们也可以为首页添加一个相似的测试:

    1. def test_blogpost_create_with_show_in_homepage(self):
    2. Blogpost.objects.create(title='hello', author='admin', slug='this_is_a_test', body='This is a blog',
    3. posted=datetime.now)
    4. response = self.client.get('/')
    5. self.assertIn(b'This is a blog', response.content)

    我们用同样的方法创建了一篇博客,然后在首页测试返回的内容中是否含有This is a blog