Django blog tutorial - the next generation - part 4

Published by at 15th February 2014 5:45 pm

Hello again! As promised, in this instalment we'll implement categories and tags, as well as an RSS feed.

As usual, we need to switch into our virtualenv:

$ source venv/bin/activate

Categories

It's worth taking a little time at this point to set out what we mean by categories and tags in this case, as the two can be very similar. In this case, we'll use the following criteria:

  • A post can have only one category, or none, but a category can be applied to any number of posts
  • A post can have any number of tags, and a tag can be applied to any number of posts

If you're not too familiar with relational database theory, the significance of this may not be apparent, so here's a quick explanation. Because the categories are limited to one per post, the relationship between a post and a category is known as one-to-many. In other words, one post can only have one category, but one category can have many posts. You can therefore define the categories in one table in your database, and refer to them by their ID (the reference to the category in the post table is referred to as a foreign key).

As usual, we will write a test first. Open up blogengine/tests.py and edit the line importing the Post model as follows:

from blogengine.models import Post, Category

Also, add the following method before test_create_post:

1 def test_create_category(self):
2 # Create the category
3 category = Category()
4
5 # Add attributes
6 category.name = 'python'
7 category.description = 'The Python programming language'
8
9 # Save it
10 category.save()
11
12 # Check we can find it
13 all_categories = Category.objects.all()
14 self.assertEquals(len(all_categories), 1)
15 only_category = all_categories[0]
16 self.assertEquals(only_category, category)
17
18 # Check attributes
19 self.assertEquals(only_category.name, 'python')
20 self.assertEquals(only_category.description, 'The Python programming language')

This test checks that we can create categories. But categories aren't much use unless we can actually apply them to our posts. So we need to edit our Post model as well, and to do so we need to have a test in place first. Edit the test_create_post method as follows:

1 def test_create_post(self):
2 # Create the category
3 category = Category()
4 category.name = 'python'
5 category.description = 'The Python programming language'
6 category.save()
7
8 # Create the author
9 author = User.objects.create_user('testuser', 'user@example.com', 'password')
10 author.save()
11
12 # Create the site
13 site = Site()
14 site.name = 'example.com'
15 site.domain = 'example.com'
16 site.save()
17
18 # Create the post
19 post = Post()
20
21 # Set the attributes
22 post.title = 'My first post'
23 post.text = 'This is my first blog post'
24 post.slug = 'my-first-post'
25 post.pub_date = timezone.now()
26 post.author = author
27 post.site = site
28 post.category = category
29
30 # Save it
31 post.save()
32
33 # Check we can find it
34 all_posts = Post.objects.all()
35 self.assertEquals(len(all_posts), 1)
36 only_post = all_posts[0]
37 self.assertEquals(only_post, post)
38
39 # Check attributes
40 self.assertEquals(only_post.title, 'My first post')
41 self.assertEquals(only_post.text, 'This is my first blog post')
42 self.assertEquals(only_post.slug, 'my-first-post')
43 self.assertEquals(only_post.site.name, 'example.com')
44 self.assertEquals(only_post.site.domain, 'example.com')
45 self.assertEquals(only_post.pub_date.day, post.pub_date.day)
46 self.assertEquals(only_post.pub_date.month, post.pub_date.month)
47 self.assertEquals(only_post.pub_date.year, post.pub_date.year)
48 self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)
49 self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)
50 self.assertEquals(only_post.pub_date.second, post.pub_date.second)
51 self.assertEquals(only_post.author.username, 'testuser')
52 self.assertEquals(only_post.author.email, 'user@example.com')
53 self.assertEquals(only_post.category.name, 'python')
54 self.assertEquals(only_post.category.description, 'The Python programming language')

What we're doing here is adding a category attribute to the posts. This attribute contains a reference to a Category object.

Now, we also want to test adding, editing, and deleting a category from the admin interface. Add this code to the AdminTest class:

1 def test_create_category(self):
2 # Log in
3 self.client.login(username='bobsmith', password="password")
4
5 # Check response code
6 response = self.client.get('/admin/blogengine/category/add/')
7 self.assertEquals(response.status_code, 200)
8
9 # Create the new category
10 response = self.client.post('/admin/blogengine/category/add/', {
11 'name': 'python',
12 'description': 'The Python programming language'
13 },
14 follow=True
15 )
16 self.assertEquals(response.status_code, 200)
17
18 # Check added successfully
19 self.assertTrue('added successfully' in response.content)
20
21 # Check new category now in database
22 all_categories = Category.objects.all()
23 self.assertEquals(len(all_categories), 1)
24
25 def test_edit_category(self):
26 # Create the category
27 category = Category()
28 category.name = 'python'
29 category.description = 'The Python programming language'
30 category.save()
31
32 # Log in
33 self.client.login(username='bobsmith', password="password")
34
35 # Edit the category
36 response = self.client.post('/admin/blogengine/category/1/', {
37 'name': 'perl',
38 'description': 'The Perl programming language'
39 }, follow=True)
40 self.assertEquals(response.status_code, 200)
41
42 # Check changed successfully
43 self.assertTrue('changed successfully' in response.content)
44
45 # Check category amended
46 all_categories = Category.objects.all()
47 self.assertEquals(len(all_categories), 1)
48 only_category = all_categories[0]
49 self.assertEquals(only_category.name, 'perl')
50 self.assertEquals(only_category.description, 'The Perl programming language')
51
52 def test_delete_category(self):
53 # Create the category
54 category = Category()
55 category.name = 'python'
56 category.description = 'The Python programming language'
57 category.save()
58
59 # Log in
60 self.client.login(username='bobsmith', password="password")
61
62 # Delete the category
63 response = self.client.post('/admin/blogengine/category/1/delete/', {
64 'post': 'yes'
65 }, follow=True)
66 self.assertEquals(response.status_code, 200)
67
68 # Check deleted successfully
69 self.assertTrue('deleted successfully' in response.content)
70
71 # Check category deleted
72 all_categories = Category.objects.all()
73 self.assertEquals(len(all_categories), 0)

This is very similar to the prior code for the posts, and just checks we can create categories via the admin. We also need to check we can apply these categories to posts, and that they don't break the existing tests:

1 def test_create_post(self):
2 # Create the category
3 category = Category()
4 category.name = 'python'
5 category.description = 'The Python programming language'
6 category.save()
7
8 # Log in
9 self.client.login(username='bobsmith', password="password")
10
11 # Check response code
12 response = self.client.get('/admin/blogengine/post/add/')
13 self.assertEquals(response.status_code, 200)
14
15 # Create the new post
16 response = self.client.post('/admin/blogengine/post/add/', {
17 'title': 'My first post',
18 'text': 'This is my first post',
19 'pub_date_0': '2013-12-28',
20 'pub_date_1': '22:00:04',
21 'slug': 'my-first-post',
22 'site': '1',
23 'category': '1'
24 },
25 follow=True
26 )
27 self.assertEquals(response.status_code, 200)
28
29 # Check added successfully
30 self.assertTrue('added successfully' in response.content)
31
32 # Check new post now in database
33 all_posts = Post.objects.all()
34 self.assertEquals(len(all_posts), 1)
35
36 def test_edit_post(self):
37 # Create the category
38 category = Category()
39 category.name = 'python'
40 category.description = 'The Python programming language'
41 category.save()
42
43 # Create the author
44 author = User.objects.create_user('testuser', 'user@example.com', 'password')
45 author.save()
46
47 # Create the site
48 site = Site()
49 site.name = 'example.com'
50 site.domain = 'example.com'
51 site.save()
52
53 # Create the post
54 post = Post()
55 post.title = 'My first post'
56 post.text = 'This is my first blog post'
57 post.slug = 'my-first-post'
58 post.pub_date = timezone.now()
59 post.author = author
60 post.site = site
61 post.save()
62
63 # Log in
64 self.client.login(username='bobsmith', password="password")
65
66 # Edit the post
67 response = self.client.post('/admin/blogengine/post/1/', {
68 'title': 'My second post',
69 'text': 'This is my second blog post',
70 'pub_date_0': '2013-12-28',
71 'pub_date_1': '22:00:04',
72 'slug': 'my-second-post',
73 'site': '1',
74 'category': '1'
75 },
76 follow=True
77 )
78 self.assertEquals(response.status_code, 200)
79
80 # Check changed successfully
81 self.assertTrue('changed successfully' in response.content)
82
83 # Check post amended
84 all_posts = Post.objects.all()
85 self.assertEquals(len(all_posts), 1)
86 only_post = all_posts[0]
87 self.assertEquals(only_post.title, 'My second post')
88 self.assertEquals(only_post.text, 'This is my second blog post')
89
90 def test_delete_post(self):
91 # Create the category
92 category = Category()
93 category.name = 'python'
94 category.description = 'The Python programming language'
95 category.save()
96
97 # Create the author
98 author = User.objects.create_user('testuser', 'user@example.com', 'password')
99 author.save()
100
101 # Create the site
102 site = Site()
103 site.name = 'example.com'
104 site.domain = 'example.com'
105 site.save()
106
107 # Create the post
108 post = Post()
109 post.title = 'My first post'
110 post.text = 'This is my first blog post'
111 post.slug = 'my-first-post'
112 post.pub_date = timezone.now()
113 post.site = site
114 post.author = author
115 post.category = category
116 post.save()
117
118 # Check new post saved
119 all_posts = Post.objects.all()
120 self.assertEquals(len(all_posts), 1)
121
122 # Log in
123 self.client.login(username='bobsmith', password="password")
124
125 # Delete the post
126 response = self.client.post('/admin/blogengine/post/1/delete/', {
127 'post': 'yes'
128 }, follow=True)
129 self.assertEquals(response.status_code, 200)
130
131 # Check deleted successfully
132 self.assertTrue('deleted successfully' in response.content)
133
134 # Check post deleted
135 all_posts = Post.objects.all()
136 self.assertEquals(len(all_posts), 0)

Here we basically take our existing post tests in the admin interface and add the category to them. Finally, we edit the PostViewTest class to include categories:

1class PostViewTest(BaseAcceptanceTest):
2 def test_index(self):
3 # Create the category
4 category = Category()
5 category.name = 'python'
6 category.description = 'The Python programming language'
7 category.save()
8
9 # Create the author
10 author = User.objects.create_user('testuser', 'user@example.com', 'password')
11 author.save()
12
13 # Create the site
14 site = Site()
15 site.name = 'example.com'
16 site.domain = 'example.com'
17 site.save()
18
19 # Create the post
20 post = Post()
21 post.title = 'My first post'
22 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
23 post.slug = 'my-first-post'
24 post.pub_date = timezone.now()
25 post.author = author
26 post.site = site
27 post.category = category
28 post.save()
29
30 # Check new post saved
31 all_posts = Post.objects.all()
32 self.assertEquals(len(all_posts), 1)
33
34 # Fetch the index
35 response = self.client.get('/')
36 self.assertEquals(response.status_code, 200)
37
38 # Check the post title is in the response
39 self.assertTrue(post.title in response.content)
40
41 # Check the post text is in the response
42 self.assertTrue(markdown.markdown(post.text) in response.content)
43
44 # Check the post date is in the response
45 self.assertTrue(str(post.pub_date.year) in response.content)
46 self.assertTrue(post.pub_date.strftime('%b') in response.content)
47 self.assertTrue(str(post.pub_date.day) in response.content)
48
49 # Check the link is marked up properly
50 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
51
52 def test_post_page(self):
53 # Create the category
54 category = Category()
55 category.name = 'python'
56 category.description = 'The Python programming language'
57 category.save()
58
59 # Create the author
60 author = User.objects.create_user('testuser', 'user@example.com', 'password')
61 author.save()
62
63 # Create the site
64 site = Site()
65 site.name = 'example.com'
66 site.domain = 'example.com'
67 site.save()
68
69 # Create the post
70 post = Post()
71 post.title = 'My first post'
72 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
73 post.slug = 'my-first-post'
74 post.pub_date = timezone.now()
75 post.author = author
76 post.site = site
77 post.category = category
78 post.save()
79
80 # Check new post saved
81 all_posts = Post.objects.all()
82 self.assertEquals(len(all_posts), 1)
83 only_post = all_posts[0]
84 self.assertEquals(only_post, post)
85
86 # Get the post URL
87 post_url = only_post.get_absolute_url()
88
89 # Fetch the post
90 response = self.client.get(post_url)
91 self.assertEquals(response.status_code, 200)
92
93 # Check the post title is in the response
94 self.assertTrue(post.title in response.content)
95
96 # Check the post text is in the response
97 self.assertTrue(markdown.markdown(post.text) in response.content)
98
99 # Check the post date is in the response
100 self.assertTrue(str(post.pub_date.year) in response.content)
101 self.assertTrue(post.pub_date.strftime('%b') in response.content)
102 self.assertTrue(str(post.pub_date.day) in response.content)
103
104 # Check the link is marked up properly
105 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

Now it's time to run our tests:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3E
4======================================================================
5ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)
6----------------------------------------------------------------------
7ImportError: Failed to import test module: blogengine.tests
8Traceback (most recent call last):
9 File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 254, in _find_tests
10 module = self._get_module_from_name(name)
11 File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
12 __import__(name)
13 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 3, in <module>
14 from blogengine.models import Post, Category
15ImportError: cannot import name Category
16
17
18----------------------------------------------------------------------
19Ran 1 test in 0.000s
20
21FAILED (errors=1)
22Destroying test database for alias 'default'...

This is the expected result. We need to create our Category model. So let's do that:

1from django.db import models
2from django.contrib.auth.models import User
3from django.contrib.sites.models import Site
4
5# Create your models here.
6class Category(models.Model):
7 name = models.CharField(max_length=200)
8 description = models.TextField()
9
10
11class Post(models.Model):
12 title = models.CharField(max_length=200)
13 pub_date = models.DateTimeField()
14 text = models.TextField()
15 slug = models.SlugField(max_length=40, unique=True)
16 author = models.ForeignKey(User)
17 site = models.ForeignKey(Site)
18 category = models.ForeignKey(Category, blank=True, null=True)
19
20 def get_absolute_url(self):
21 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
22
23 def __unicode__(self):
24 return self.title
25
26 class Meta:
27 ordering = ["-pub_date"]

Note that we add Category before Post - this is because Category is a foreign key in Post, and must be defined in order to be used. Also, note that we add the category attribute as a ForeignKey field, like User and Site, indicating that it is an item in another table being references.

We also allow for category to be blank or null, so the user does not have to apply a category if they don't wish to.

If we run our tests, they should still fail:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3EEFEEEEE...EE
4======================================================================
5ERROR: test_create_category (blogengine.tests.PostTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 20, in test_create_category
9 category.save()
10 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
11 force_update=force_update, update_fields=update_fields)
12 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
13 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
14 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
15 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
17 using=using, raw=raw)
18 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
19 return insert_query(self.model, objs, fields, **kwargs)
20 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
21 return query.get_compiler(using=using).execute_sql(return_id)
22 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
23 cursor.execute(sql, params)
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
25 return self.cursor.execute(sql, params)
26 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
27 six.reraise(dj_exc_type, dj_exc_value, traceback)
28 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
29 return self.cursor.execute(sql, params)
30 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
31 return Database.Cursor.execute(self, query, params)
32OperationalError: no such table: blogengine_category
33
34======================================================================
35ERROR: test_create_post (blogengine.tests.PostTest)
36----------------------------------------------------------------------
37Traceback (most recent call last):
38 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 37, in test_create_post
39 category.save()
40 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
41 force_update=force_update, update_fields=update_fields)
42 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
43 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
44 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
45 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
46 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
47 using=using, raw=raw)
48 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
49 return insert_query(self.model, objs, fields, **kwargs)
50 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
51 return query.get_compiler(using=using).execute_sql(return_id)
52 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
53 cursor.execute(sql, params)
54 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
55 return self.cursor.execute(sql, params)
56 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
57 six.reraise(dj_exc_type, dj_exc_value, traceback)
58 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
59 return self.cursor.execute(sql, params)
60 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
61 return Database.Cursor.execute(self, query, params)
62OperationalError: no such table: blogengine_category
63
64======================================================================
65ERROR: test_create_post (blogengine.tests.AdminTest)
66----------------------------------------------------------------------
67Traceback (most recent call last):
68 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 215, in test_create_post
69 category.save()
70 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
71 force_update=force_update, update_fields=update_fields)
72 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
73 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
74 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
75 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
76 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
77 using=using, raw=raw)
78 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
79 return insert_query(self.model, objs, fields, **kwargs)
80 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
81 return query.get_compiler(using=using).execute_sql(return_id)
82 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
83 cursor.execute(sql, params)
84 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
85 return self.cursor.execute(sql, params)
86 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
87 six.reraise(dj_exc_type, dj_exc_value, traceback)
88 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
89 return self.cursor.execute(sql, params)
90 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
91 return Database.Cursor.execute(self, query, params)
92OperationalError: no such table: blogengine_category
93
94======================================================================
95ERROR: test_delete_category (blogengine.tests.AdminTest)
96----------------------------------------------------------------------
97Traceback (most recent call last):
98 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 192, in test_delete_category
99 category.save()
100 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
101 force_update=force_update, update_fields=update_fields)
102 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
103 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
104 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
105 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
106 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
107 using=using, raw=raw)
108 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
109 return insert_query(self.model, objs, fields, **kwargs)
110 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
111 return query.get_compiler(using=using).execute_sql(return_id)
112 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
113 cursor.execute(sql, params)
114 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
115 return self.cursor.execute(sql, params)
116 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
117 six.reraise(dj_exc_type, dj_exc_value, traceback)
118 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
119 return self.cursor.execute(sql, params)
120 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
121 return Database.Cursor.execute(self, query, params)
122OperationalError: no such table: blogengine_category
123
124======================================================================
125ERROR: test_delete_post (blogengine.tests.AdminTest)
126----------------------------------------------------------------------
127Traceback (most recent call last):
128 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 304, in test_delete_post
129 category.save()
130 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
131 force_update=force_update, update_fields=update_fields)
132 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
133 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
134 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
135 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
136 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
137 using=using, raw=raw)
138 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
139 return insert_query(self.model, objs, fields, **kwargs)
140 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
141 return query.get_compiler(using=using).execute_sql(return_id)
142 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
143 cursor.execute(sql, params)
144 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
145 return self.cursor.execute(sql, params)
146 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
147 six.reraise(dj_exc_type, dj_exc_value, traceback)
148 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
149 return self.cursor.execute(sql, params)
150 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
151 return Database.Cursor.execute(self, query, params)
152OperationalError: no such table: blogengine_category
153
154======================================================================
155ERROR: test_edit_category (blogengine.tests.AdminTest)
156----------------------------------------------------------------------
157Traceback (most recent call last):
158 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 165, in test_edit_category
159 category.save()
160 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
161 force_update=force_update, update_fields=update_fields)
162 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
163 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
164 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
165 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
166 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
167 using=using, raw=raw)
168 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
169 return insert_query(self.model, objs, fields, **kwargs)
170 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
171 return query.get_compiler(using=using).execute_sql(return_id)
172 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
173 cursor.execute(sql, params)
174 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
175 return self.cursor.execute(sql, params)
176 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
177 six.reraise(dj_exc_type, dj_exc_value, traceback)
178 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
179 return self.cursor.execute(sql, params)
180 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
181 return Database.Cursor.execute(self, query, params)
182OperationalError: no such table: blogengine_category
183
184======================================================================
185ERROR: test_edit_post (blogengine.tests.AdminTest)
186----------------------------------------------------------------------
187Traceback (most recent call last):
188 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 250, in test_edit_post
189 category.save()
190 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
191 force_update=force_update, update_fields=update_fields)
192 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
193 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
194 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
195 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
196 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
197 using=using, raw=raw)
198 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
199 return insert_query(self.model, objs, fields, **kwargs)
200 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
201 return query.get_compiler(using=using).execute_sql(return_id)
202 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
203 cursor.execute(sql, params)
204 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
205 return self.cursor.execute(sql, params)
206 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
207 six.reraise(dj_exc_type, dj_exc_value, traceback)
208 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
209 return self.cursor.execute(sql, params)
210 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
211 return Database.Cursor.execute(self, query, params)
212OperationalError: no such table: blogengine_category
213
214======================================================================
215ERROR: test_index (blogengine.tests.PostViewTest)
216----------------------------------------------------------------------
217Traceback (most recent call last):
218 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 353, in test_index
219 category.save()
220 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
221 force_update=force_update, update_fields=update_fields)
222 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
223 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
224 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
225 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
226 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
227 using=using, raw=raw)
228 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
229 return insert_query(self.model, objs, fields, **kwargs)
230 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
231 return query.get_compiler(using=using).execute_sql(return_id)
232 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
233 cursor.execute(sql, params)
234 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
235 return self.cursor.execute(sql, params)
236 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
237 six.reraise(dj_exc_type, dj_exc_value, traceback)
238 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
239 return self.cursor.execute(sql, params)
240 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
241 return Database.Cursor.execute(self, query, params)
242OperationalError: no such table: blogengine_category
243
244======================================================================
245ERROR: test_post_page (blogengine.tests.PostViewTest)
246----------------------------------------------------------------------
247Traceback (most recent call last):
248 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 403, in test_post_page
249 category.save()
250 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
251 force_update=force_update, update_fields=update_fields)
252 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
253 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
254 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
255 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
256 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
257 using=using, raw=raw)
258 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
259 return insert_query(self.model, objs, fields, **kwargs)
260 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
261 return query.get_compiler(using=using).execute_sql(return_id)
262 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
263 cursor.execute(sql, params)
264 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
265 return self.cursor.execute(sql, params)
266 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
267 six.reraise(dj_exc_type, dj_exc_value, traceback)
268 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
269 return self.cursor.execute(sql, params)
270 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
271 return Database.Cursor.execute(self, query, params)
272OperationalError: no such table: blogengine_category
273
274======================================================================
275FAIL: test_create_category (blogengine.tests.AdminTest)
276----------------------------------------------------------------------
277Traceback (most recent call last):
278 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 142, in test_create_category
279 self.assertEquals(response.status_code, 200)
280AssertionError: 404 != 200
281
282----------------------------------------------------------------------
283Ran 13 tests in 3.393s
284
285FAILED (failures=1, errors=9)
286Destroying test database for alias 'default'...

The category table hasn't yet been created, so we need to use South to create and run the migrations:

1$ python manage.py schemamigration --auto blogengine
2$ python manage.py migrate

If we then run our tests again, some of them will still fail:

1Creating test database for alias 'default'...
2..F.F.F......
3======================================================================
4FAIL: test_create_category (blogengine.tests.AdminTest)
5----------------------------------------------------------------------
6Traceback (most recent call last):
7 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 142, in test_create_category
8 self.assertEquals(response.status_code, 200)
9AssertionError: 404 != 200
10
11======================================================================
12FAIL: test_delete_category (blogengine.tests.AdminTest)
13----------------------------------------------------------------------
14Traceback (most recent call last):
15 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 201, in test_delete_category
16 self.assertEquals(response.status_code, 200)
17AssertionError: 404 != 200
18
19======================================================================
20FAIL: test_edit_category (blogengine.tests.AdminTest)
21----------------------------------------------------------------------
22Traceback (most recent call last):
23 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 175, in test_edit_category
24 self.assertEquals(response.status_code, 200)
25AssertionError: 404 != 200
26
27----------------------------------------------------------------------
28Ran 13 tests in 4.047s
29
30FAILED (failures=3)
31Destroying test database for alias 'default'...

That's because we haven't registered the categories in the admin. So, that's our next job:

1import models
2from django.contrib import admin
3from django.contrib.auth.models import User
4
5class PostAdmin(admin.ModelAdmin):
6 prepopulated_fields = {"slug": ("title",)}
7 exclude = ('author',)
8
9 def save_model(self, request, obj, form, change):
10 obj.author = request.user
11 obj.save()
12
13admin.site.register(models.Category)
14admin.site.register(models.Post, PostAdmin)

Now we try again:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3.............
4----------------------------------------------------------------------
5Ran 13 tests in 4.092s
6
7OK
8Destroying test database for alias 'default'...

It passes! Let's do a quick sense check before committing. Run the server:

$ python manage.py runserver

If you visit the admin, you'll see the text for category is Categorys, which is incorrect. We also don't have a good representation of the category in the admin. Let's fix that:

1from django.db import models
2from django.contrib.auth.models import User
3from django.contrib.sites.models import Site
4
5# Create your models here.
6class Category(models.Model):
7 name = models.CharField(max_length=200)
8 description = models.TextField()
9
10 def __unicode__(self):
11 return self.name
12
13 class Meta:
14 verbose_name_plural = 'categories'
15
16
17class Post(models.Model):
18 title = models.CharField(max_length=200)
19 pub_date = models.DateTimeField()
20 text = models.TextField()
21 slug = models.SlugField(max_length=40, unique=True)
22 author = models.ForeignKey(User)
23 site = models.ForeignKey(Site)
24 category = models.ForeignKey(Category, blank=True, null=True)
25
26 def get_absolute_url(self):
27 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
28
29 def __unicode__(self):
30 return self.title
31
32 class Meta:
33 ordering = ["-pub_date"]

Let's commit our changes:

1$ git add blogengine/
2$ git commit -m 'Implemented categories'

Now, as yet our categories don't actually do all that much. We would like to be able to:

  • List all posts under a category
  • Show the post category at the base of the post

So, let's implement that. First, as usual, we implement tests first:

1class PostViewTest(BaseAcceptanceTest):
2 def test_index(self):
3 # Create the category
4 category = Category()
5 category.name = 'python'
6 category.description = 'The Python programming language'
7 category.save()
8
9 # Create the author
10 author = User.objects.create_user('testuser', 'user@example.com', 'password')
11 author.save()
12
13 # Create the site
14 site = Site()
15 site.name = 'example.com'
16 site.domain = 'example.com'
17 site.save()
18
19 # Create the post
20 post = Post()
21 post.title = 'My first post'
22 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
23 post.slug = 'my-first-post'
24 post.pub_date = timezone.now()
25 post.author = author
26 post.site = site
27 post.category = category
28 post.save()
29
30 # Check new post saved
31 all_posts = Post.objects.all()
32 self.assertEquals(len(all_posts), 1)
33
34 # Fetch the index
35 response = self.client.get('/')
36 self.assertEquals(response.status_code, 200)
37
38 # Check the post title is in the response
39 self.assertTrue(post.title in response.content)
40
41 # Check the post text is in the response
42 self.assertTrue(markdown.markdown(post.text) in response.content)
43
44 # Check the post category is in the response
45 self.assertTrue(post.category.name in response.content)
46
47 # Check the post date is in the response
48 self.assertTrue(str(post.pub_date.year) in response.content)
49 self.assertTrue(post.pub_date.strftime('%b') in response.content)
50 self.assertTrue(str(post.pub_date.day) in response.content)
51
52 # Check the link is marked up properly
53 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
54
55 def test_post_page(self):
56 # Create the category
57 category = Category()
58 category.name = 'python'
59 category.description = 'The Python programming language'
60 category.save()
61
62 # Create the author
63 author = User.objects.create_user('testuser', 'user@example.com', 'password')
64 author.save()
65
66 # Create the site
67 site = Site()
68 site.name = 'example.com'
69 site.domain = 'example.com'
70 site.save()
71
72 # Create the post
73 post = Post()
74 post.title = 'My first post'
75 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
76 post.slug = 'my-first-post'
77 post.pub_date = timezone.now()
78 post.author = author
79 post.site = site
80 post.category = category
81 post.save()
82
83 # Check new post saved
84 all_posts = Post.objects.all()
85 self.assertEquals(len(all_posts), 1)
86 only_post = all_posts[0]
87 self.assertEquals(only_post, post)
88
89 # Get the post URL
90 post_url = only_post.get_absolute_url()
91
92 # Fetch the post
93 response = self.client.get(post_url)
94 self.assertEquals(response.status_code, 200)
95
96 # Check the post title is in the response
97 self.assertTrue(post.title in response.content)
98
99 # Check the post category is in the response
100 self.assertTrue(post.category.name in response.content)
101
102 # Check the post text is in the response
103 self.assertTrue(markdown.markdown(post.text) in response.content)
104
105 # Check the post date is in the response
106 self.assertTrue(str(post.pub_date.year) in response.content)
107 self.assertTrue(post.pub_date.strftime('%b') in response.content)
108 self.assertTrue(str(post.pub_date.day) in response.content)
109
110 # Check the link is marked up properly
111 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

All we do here is assert that for both the post pages and the index, the text from the category name is shown in the response. We also need to check the category-specific route works. Add this method to PostViewTest:

1 def test_category_page(self):
2 # Create the category
3 category = Category()
4 category.name = 'python'
5 category.description = 'The Python programming language'
6 category.save()
7
8 # Create the author
9 author = User.objects.create_user('testuser', 'user@example.com', 'password')
10 author.save()
11
12 # Create the site
13 site = Site()
14 site.name = 'example.com'
15 site.domain = 'example.com'
16 site.save()
17
18 # Create the post
19 post = Post()
20 post.title = 'My first post'
21 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
22 post.slug = 'my-first-post'
23 post.pub_date = timezone.now()
24 post.author = author
25 post.site = site
26 post.category = category
27 post.save()
28
29 # Check new post saved
30 all_posts = Post.objects.all()
31 self.assertEquals(len(all_posts), 1)
32 only_post = all_posts[0]
33 self.assertEquals(only_post, post)
34
35 # Get the category URL
36 category_url = post.category.get_absolute_url()
37
38 # Fetch the category
39 response = self.client.get(category_url)
40 self.assertEquals(response.status_code, 200)
41
42 # Check the category name is in the response
43 self.assertTrue(post.category.name in response.content)
44
45 # Check the post text is in the response
46 self.assertTrue(markdown.markdown(post.text) in response.content)
47
48 # Check the post date is in the response
49 self.assertTrue(str(post.pub_date.year) in response.content)
50 self.assertTrue(post.pub_date.strftime('%b') in response.content)
51 self.assertTrue(str(post.pub_date.day) in response.content)
52
53 # Check the link is marked up properly
54 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

This is very similar to the previous tests, but fetches the absolute URL for the category, and ensures the category name and post content are shown. Now, let's run our new tests:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3...........EFF
4======================================================================
5ERROR: test_category_page (blogengine.tests.PostViewTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 494, in test_category_page
9 category_url = post.category.get_absolute_url()
10AttributeError: 'Category' object has no attribute 'get_absolute_url'
11
12======================================================================
13FAIL: test_index (blogengine.tests.PostViewTest)
14----------------------------------------------------------------------
15Traceback (most recent call last):
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 391, in test_index
17 self.assertTrue(post.category.name in response.content)
18AssertionError: False is not true
19
20======================================================================
21FAIL: test_post_page (blogengine.tests.PostViewTest)
22----------------------------------------------------------------------
23Traceback (most recent call last):
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 446, in test_post_page
25 self.assertTrue(post.category.name in response.content)
26AssertionError: False is not true
27
28----------------------------------------------------------------------
29Ran 14 tests in 5.017s
30
31FAILED (failures=2, errors=1)
32Destroying test database for alias 'default'...

Let's take a look at why they failed. test_category_page failed because the Category object had no method get_absolute_url. So we need to implement one. To do so, we really need to add a slug field, like the posts already have. Ideally, we want this to be populated automatically, but with the option to create one manually. So, edit the models as follows:

1from django.db import models
2from django.contrib.auth.models import User
3from django.contrib.sites.models import Site
4from django.utils.text import slugify
5
6# Create your models here.
7class Category(models.Model):
8 name = models.CharField(max_length=200)
9 description = models.TextField()
10 slug = models.SlugField(max_length=40, unique=True, blank=True, null=True)
11
12 def save(self):
13 if not self.slug:
14 self.slug = slugify(unicode(self.name))
15 super(Category, self).save()
16
17 def get_absolute_url(self):
18 return "/category/%s/" % (self.slug)
19
20 def __unicode__(self):
21 return self.name
22
23 class Meta:
24 verbose_name_plural = 'categories'
25
26
27class Post(models.Model):
28 title = models.CharField(max_length=200)
29 pub_date = models.DateTimeField()
30 text = models.TextField()
31 slug = models.SlugField(max_length=40, unique=True)
32 author = models.ForeignKey(User)
33 site = models.ForeignKey(Site)
34 category = models.ForeignKey(Category, blank=True, null=True)
35
36 def get_absolute_url(self):
37 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
38
39 def __unicode__(self):
40 return self.title
41
42 class Meta:
43 ordering = ["-pub_date"]

We're adding the slug attribute to the Category model here. However, we're also overriding the save method to detect if the slug is set, and if not, to create a slug using the slugify function, and set it as the category's slug. We also define an absolute URL for the category.

Now, if you run the tests, they will fail because we haven't made the changes to the database. So, we use South again:

$ python manage.py schemamigration --auto blogengine

Then run the migration:

$ python manage.py migrate

Now, running our tests will show that the tables are in place, but we still have some work to do. The index and post pages don't show our categories, so we'll fix that. First, we'll fix our post list:

1{% extends "blogengine/includes/base.html" %}
2
3 {% load custom_markdown %}
4
5 {% block content %}
6 {% for post in object_list %}
7 <div class="post">
8 <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
9 <h3>{{ post.pub_date }}</h3>
10 {{ post.text|custom_markdown }}
11 </div>
12 <a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a>
13 {% endfor %}
14
15 {% if page_obj.has_previous %}
16 <a href="/{{ page_obj.previous_page_number }}/">Previous Page</a>
17 {% endif %}
18 {% if page_obj.has_next %}
19 <a href="/{{ page_obj.next_page_number }}/">Next Page</a>
20 {% endif %}
21
22 {% endblock %}

Next, we'll take care of our post detail page:

1{% extends "blogengine/includes/base.html" %}
2
3 {% load custom_markdown %}
4
5 {% block content %}
6 <div class="post">
7 <h1>{{ object.title }}</h1>
8 <h3>{{ object.pub_date }}</h3>
9 {{ object.text|custom_markdown }}
10 <a href="{{ object.category.get_absolute_url }}">{{ object.category.name }}</a>
11
12 <h4>Comments</h4>
13 <div class="fb-comments" data-href="http://{{ post.site }}{{ post.get_absolute_url }}" data-width="470" data-num-posts="10"></div>
14
15 </div>
16
17 {% endblock %}

Note that in both cases we include a link to the category URL.

Now, we should only have one failing test outstanding - the category page. For this, generic views aren't sufficient as we need to limit the queryset to only show those posts with a specific category. Fortunately, we can extend Django's generic views to add this functionality. First, we edit our URLconfs:

1from django.conf.urls import patterns, url
2from django.views.generic import ListView, DetailView
3from blogengine.models import Post, Category
4from blogengine.views import CategoryListView
5
6urlpatterns = patterns('',
7 # Index
8 url(r'^(?P<page>\d+)?/?$', ListView.as_view(
9 model=Post,
10 paginate_by=5,
11 )),
12
13 # Individual posts
14 url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
15 model=Post,
16 )),
17
18 # Categories
19 url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
20 paginate_by=5,
21 model=Category,
22 )),
23)

Note we import a new view from blogengine.views called CategoryListView. Next, we create that listview:

1from django.shortcuts import render
2from django.views.generic import ListView
3from blogengine.models import Category, Post
4
5# Create your views here.
6class CategoryListView(ListView):
7 def get_queryset(self):
8 slug = self.kwargs['slug']
9 try:
10 category = Category.objects.get(slug=slug)
11 return Post.objects.filter(category=category)
12 except Category.DoesNotExist:
13 return Post.objects.none()

This is quite simple. We import the ListView, as well as our models. Then we extend ListView by getting the slug from the request, fetching the appropriate category, and returning only those posts that have that category. If the category does not exist, we return the empty Post object list. We haven't had to set the template manually as it is inherited from ListView.

If you run the tests, they should now pass:

1Creating test database for alias 'default'...
2..............
3----------------------------------------------------------------------
4Ran 14 tests in 5.083s
5
6OK
7Destroying test database for alias 'default'...

So let's commit our changes:

1$ git add blogengine/ templates/
2$ git commit -m 'Categories are now shown'

Tags

Tags are fairly similar to categories, but more complex. The relationship they have is called many-to-many - in other words, a tag can be applied to many posts, and one post can have many tags. This is more difficult to model with a relational database. The usual way to do so is to create an intermediate table between the posts and tags, to identify mappings between the two. Fortunately, Django makes this quite easy.

Let's write the tests for our tagging system. As with the categories, we'll write the tests for creating and editing them first, and add in tests for them being visible later. First we'll create a test for creating a new tag object:

1 def test_create_tag(self):
2 # Create the tag
3 tag = Tag()
4
5 # Add attributes
6 tag.name = 'python'
7 tag.description = 'The Python programming language'
8
9 # Save it
10 tag.save()
11
12 # Check we can find it
13 all_tags = Tag.objects.all()
14 self.assertEquals(len(all_tags), 1)
15 only_tag = all_tags[0]
16 self.assertEquals(only_tag, tag)
17
18 # Check attributes
19 self.assertEquals(only_tag.name, 'python')
20 self.assertEquals(only_tag.description, 'The Python programming language')

Next, we'll amend the test for creating a post to include tags:

1 def test_create_post(self):
2 # Create the category
3 category = Category()
4 category.name = 'python'
5 category.description = 'The Python programming language'
6 category.save()
7
8 # Create the tag
9 tag = Tag()
10 tag.name = 'python'
11 tag.description = 'The Python programming language'
12 tag.save()
13
14 # Create the author
15 author = User.objects.create_user('testuser', 'user@example.com', 'password')
16 author.save()
17
18 # Create the site
19 site = Site()
20 site.name = 'example.com'
21 site.domain = 'example.com'
22 site.save()
23
24 # Create the post
25 post = Post()
26
27 # Set the attributes
28 post.title = 'My first post'
29 post.text = 'This is my first blog post'
30 post.slug = 'my-first-post'
31 post.pub_date = timezone.now()
32 post.author = author
33 post.site = site
34 post.category = category
35
36 # Save it
37 post.save()
38
39 # Add the tag
40 post.tags.add(tag)
41 post.save()
42
43 # Check we can find it
44 all_posts = Post.objects.all()
45 self.assertEquals(len(all_posts), 1)
46 only_post = all_posts[0]
47 self.assertEquals(only_post, post)
48
49 # Check attributes
50 self.assertEquals(only_post.title, 'My first post')
51 self.assertEquals(only_post.text, 'This is my first blog post')
52 self.assertEquals(only_post.slug, 'my-first-post')
53 self.assertEquals(only_post.site.name, 'example.com')
54 self.assertEquals(only_post.site.domain, 'example.com')
55 self.assertEquals(only_post.pub_date.day, post.pub_date.day)
56 self.assertEquals(only_post.pub_date.month, post.pub_date.month)
57 self.assertEquals(only_post.pub_date.year, post.pub_date.year)
58 self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)
59 self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)
60 self.assertEquals(only_post.pub_date.second, post.pub_date.second)
61 self.assertEquals(only_post.author.username, 'testuser')
62 self.assertEquals(only_post.author.email, 'user@example.com')
63 self.assertEquals(only_post.category.name, 'python')
64 self.assertEquals(only_post.category.description, 'The Python programming language')
65
66 # Check tags
67 post_tags = only_post.tags.all()
68 self.assertEquals(len(post_tags), 1)
69 only_post_tag = post_tags[0]
70 self.assertEquals(only_post_tag, tag)
71 self.assertEquals(only_post_tag.name, 'python')
72 self.assertEquals(only_post_tag.description, 'The Python programming language')

Note the difference in how we apply the tags. Because a post can have more than one tag, we can't just define post.tag in the same way. Instead, we have post.tags, which you can think of as a list, and we use the add method to add a new tag. Note also that the post must already exist before we can add a tag.

We also need to create acceptance tests for creating, editing and deleting tags:

1 def test_create_tag(self):
2 # Log in
3 self.client.login(username='bobsmith', password="password")
4
5 # Check response code
6 response = self.client.get('/admin/blogengine/tag/add/')
7 self.assertEquals(response.status_code, 200)
8
9 # Create the new tag
10 response = self.client.post('/admin/blogengine/tag/add/', {
11 'name': 'python',
12 'description': 'The Python programming language'
13 },
14 follow=True
15 )
16 self.assertEquals(response.status_code, 200)
17
18 # Check added successfully
19 self.assertTrue('added successfully' in response.content)
20
21 # Check new tag now in database
22 all_tags = Tag.objects.all()
23 self.assertEquals(len(all_tags), 1)
24
25 def test_edit_tag(self):
26 # Create the tag
27 tag = Tag()
28 tag.name = 'python'
29 tag.description = 'The Python programming language'
30 tag.save()
31
32 # Log in
33 self.client.login(username='bobsmith', password="password")
34
35 # Edit the tag
36 response = self.client.post('/admin/blogengine/tag/1/', {
37 'name': 'perl',
38 'description': 'The Perl programming language'
39 }, follow=True)
40 self.assertEquals(response.status_code, 200)
41
42 # Check changed successfully
43 self.assertTrue('changed successfully' in response.content)
44
45 # Check tag amended
46 all_tags = Tag.objects.all()
47 self.assertEquals(len(all_tags), 1)
48 only_tag = all_tags[0]
49 self.assertEquals(only_tag.name, 'perl')
50 self.assertEquals(only_tag.description, 'The Perl programming language')
51
52 def test_delete_tag(self):
53 # Create the tag
54 tag = Tag()
55 tag.name = 'python'
56 tag.description = 'The Python programming language'
57 tag.save()
58
59 # Log in
60 self.client.login(username='bobsmith', password="password")
61
62 # Delete the tag
63 response = self.client.post('/admin/blogengine/tag/1/delete/', {
64 'post': 'yes'
65 }, follow=True)
66 self.assertEquals(response.status_code, 200)
67
68 # Check deleted successfully
69 self.assertTrue('deleted successfully' in response.content)
70
71 # Check tag deleted
72 all_tags = Tag.objects.all()
73 self.assertEquals(len(all_tags), 0)

These tests are virtually identical to those for the Category objects, as we plan for our Tag objects to be very similar. Finally, we need to amend the acceptance tests for Post objects to include a tag:

1 def test_create_post(self):
2 # Create the category
3 category = Category()
4 category.name = 'python'
5 category.description = 'The Python programming language'
6 category.save()
7
8 # Create the tag
9 tag = Tag()
10 tag.name = 'python'
11 tag.description = 'The Python programming language'
12 tag.save()
13
14 # Log in
15 self.client.login(username='bobsmith', password="password")
16
17 # Check response code
18 response = self.client.get('/admin/blogengine/post/add/')
19 self.assertEquals(response.status_code, 200)
20
21 # Create the new post
22 response = self.client.post('/admin/blogengine/post/add/', {
23 'title': 'My first post',
24 'text': 'This is my first post',
25 'pub_date_0': '2013-12-28',
26 'pub_date_1': '22:00:04',
27 'slug': 'my-first-post',
28 'site': '1',
29 'category': '1',
30 'tags': '1'
31 },
32 follow=True
33 )
34 self.assertEquals(response.status_code, 200)
35
36 # Check added successfully
37 self.assertTrue('added successfully' in response.content)
38
39 # Check new post now in database
40 all_posts = Post.objects.all()
41 self.assertEquals(len(all_posts), 1)
42
43 def test_edit_post(self):
44 # Create the category
45 category = Category()
46 category.name = 'python'
47 category.description = 'The Python programming language'
48 category.save()
49
50 # Create the tag
51 tag = Tag()
52 tag.name = 'python'
53 tag.description = 'The Python programming language'
54 tag.save()
55
56 # Create the author
57 author = User.objects.create_user('testuser', 'user@example.com', 'password')
58 author.save()
59
60 # Create the site
61 site = Site()
62 site.name = 'example.com'
63 site.domain = 'example.com'
64 site.save()
65
66 # Create the post
67 post = Post()
68 post.title = 'My first post'
69 post.text = 'This is my first blog post'
70 post.slug = 'my-first-post'
71 post.pub_date = timezone.now()
72 post.author = author
73 post.site = site
74 post.save()
75 post.tags.add(tag)
76 post.save()
77
78 # Log in
79 self.client.login(username='bobsmith', password="password")
80
81 # Edit the post
82 response = self.client.post('/admin/blogengine/post/1/', {
83 'title': 'My second post',
84 'text': 'This is my second blog post',
85 'pub_date_0': '2013-12-28',
86 'pub_date_1': '22:00:04',
87 'slug': 'my-second-post',
88 'site': '1',
89 'category': '1',
90 'tags': '1'
91 },
92 follow=True
93 )
94 self.assertEquals(response.status_code, 200)
95
96 # Check changed successfully
97 self.assertTrue('changed successfully' in response.content)
98
99 # Check post amended
100 all_posts = Post.objects.all()
101 self.assertEquals(len(all_posts), 1)
102 only_post = all_posts[0]
103 self.assertEquals(only_post.title, 'My second post')
104 self.assertEquals(only_post.text, 'This is my second blog post')
105
106 def test_delete_post(self):
107 # Create the category
108 category = Category()
109 category.name = 'python'
110 category.description = 'The Python programming language'
111 category.save()
112
113 # Create the tag
114 tag = Tag()
115 tag.name = 'python'
116 tag.description = 'The Python programming language'
117 tag.save()
118
119 # Create the author
120 author = User.objects.create_user('testuser', 'user@example.com', 'password')
121 author.save()
122
123 # Create the site
124 site = Site()
125 site.name = 'example.com'
126 site.domain = 'example.com'
127 site.save()
128
129 # Create the post
130 post = Post()
131 post.title = 'My first post'
132 post.text = 'This is my first blog post'
133 post.slug = 'my-first-post'
134 post.pub_date = timezone.now()
135 post.site = site
136 post.author = author
137 post.category = category
138 post.save()
139 post.tags.add(tag)
140 post.save()
141
142 # Check new post saved
143 all_posts = Post.objects.all()
144 self.assertEquals(len(all_posts), 1)
145
146 # Log in
147 self.client.login(username='bobsmith', password="password")
148
149 # Delete the post
150 response = self.client.post('/admin/blogengine/post/1/delete/', {
151 'post': 'yes'
152 }, follow=True)
153 self.assertEquals(response.status_code, 200)
154
155 # Check deleted successfully
156 self.assertTrue('deleted successfully' in response.content)
157
158 # Check post deleted
159 all_posts = Post.objects.all()
160 self.assertEquals(len(all_posts), 0)

Here we're just adding tags to our Post objects.

Now it's time to run our tests to make sure they fail as expected:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3E
4======================================================================
5ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)
6----------------------------------------------------------------------
7ImportError: Failed to import test module: blogengine.tests
8Traceback (most recent call last):
9 File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 254, in _find_tests
10 module = self._get_module_from_name(name)
11 File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
12 __import__(name)
13 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 3, in <module>
14 from blogengine.models import Post, Category, Tag
15ImportError: cannot import name Tag
16
17
18----------------------------------------------------------------------
19Ran 1 test in 0.000s
20
21FAILED (errors=1)
22Destroying test database for alias 'default'...

So here we can't import our Tag model, because we haven't created it. So, we'll do that:

1class Tag(models.Model):
2 name = models.CharField(max_length=200)
3 description = models.TextField()
4 slug = models.SlugField(max_length=40, unique=True, blank=True, null=True)
5
6 def save(self):
7 if not self.slug:
8 self.slug = slugify(unicode(self.name))
9 super(Tag, self).save()
10
11 def get_absolute_url(self):
12 return "/tag/%s/" % (self.slug)
13
14 def __unicode__(self):
15 return self.name

Our Tag model is very much like our Category model, but we don't need to change the verbose_name_plural value, and we amend the absolute URL to show it as a tag rather than a category.

We also need to amend our Post model to include a tags field:

1class Post(models.Model):
2 title = models.CharField(max_length=200)
3 pub_date = models.DateTimeField()
4 text = models.TextField()
5 slug = models.SlugField(max_length=40, unique=True)
6 author = models.ForeignKey(User)
7 site = models.ForeignKey(Site)
8 category = models.ForeignKey(Category, blank=True, null=True)
9 tags = models.ManyToManyField(Tag)
10
11 def get_absolute_url(self):
12 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
13
14 def __unicode__(self):
15 return self.title
16
17 class Meta:
18 ordering = ["-pub_date"]

Note that tags is a ManyToManyField, and we pass through the model we wish to use, much like we did with the categories. The difference is that one tag can be applied to many posts and a post can have many tags, so we need an intermediate database table to handle the relationship between the two. With Django's ORM we can handle this quickly and easily.

Run our tests and they should still fail:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3.EE.EF.EE.EE......
4======================================================================
5ERROR: test_create_post (blogengine.tests.PostTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 64, in test_create_post
9 tag.save()
10 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
11 super(Tag, self).save()
12 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
13 force_update=force_update, update_fields=update_fields)
14 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
15 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
17 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
18 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
19 using=using, raw=raw)
20 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
21 return insert_query(self.model, objs, fields, **kwargs)
22 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
23 return query.get_compiler(using=using).execute_sql(return_id)
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
25 cursor.execute(sql, params)
26 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
27 return self.cursor.execute(sql, params)
28 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
29 six.reraise(dj_exc_type, dj_exc_value, traceback)
30 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
31 return self.cursor.execute(sql, params)
32 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
33 return Database.Cursor.execute(self, query, params)
34OperationalError: no such table: blogengine_tag
35
36======================================================================
37ERROR: test_create_tag (blogengine.tests.PostTest)
38----------------------------------------------------------------------
39Traceback (most recent call last):
40 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 41, in test_create_tag
41 tag.save()
42 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
43 super(Tag, self).save()
44 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
45 force_update=force_update, update_fields=update_fields)
46 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
47 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
48 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
49 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
50 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
51 using=using, raw=raw)
52 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
53 return insert_query(self.model, objs, fields, **kwargs)
54 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
55 return query.get_compiler(using=using).execute_sql(return_id)
56 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
57 cursor.execute(sql, params)
58 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
59 return self.cursor.execute(sql, params)
60 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
61 six.reraise(dj_exc_type, dj_exc_value, traceback)
62 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
63 return self.cursor.execute(sql, params)
64 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
65 return Database.Cursor.execute(self, query, params)
66OperationalError: no such table: blogengine_tag
67
68======================================================================
69ERROR: test_create_post (blogengine.tests.AdminTest)
70----------------------------------------------------------------------
71Traceback (most recent call last):
72 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 335, in test_create_post
73 tag.save()
74 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
75 super(Tag, self).save()
76 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
77 force_update=force_update, update_fields=update_fields)
78 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
79 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
80 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
81 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
82 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
83 using=using, raw=raw)
84 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
85 return insert_query(self.model, objs, fields, **kwargs)
86 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
87 return query.get_compiler(using=using).execute_sql(return_id)
88 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
89 cursor.execute(sql, params)
90 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
91 return self.cursor.execute(sql, params)
92 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
93 six.reraise(dj_exc_type, dj_exc_value, traceback)
94 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
95 return self.cursor.execute(sql, params)
96 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
97 return Database.Cursor.execute(self, query, params)
98OperationalError: no such table: blogengine_tag
99
100======================================================================
101ERROR: test_delete_post (blogengine.tests.AdminTest)
102----------------------------------------------------------------------
103Traceback (most recent call last):
104 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 440, in test_delete_post
105 tag.save()
106 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
107 super(Tag, self).save()
108 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
109 force_update=force_update, update_fields=update_fields)
110 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
111 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
112 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
113 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
114 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
115 using=using, raw=raw)
116 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
117 return insert_query(self.model, objs, fields, **kwargs)
118 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
119 return query.get_compiler(using=using).execute_sql(return_id)
120 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
121 cursor.execute(sql, params)
122 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
123 return self.cursor.execute(sql, params)
124 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
125 six.reraise(dj_exc_type, dj_exc_value, traceback)
126 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
127 return self.cursor.execute(sql, params)
128 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
129 return Database.Cursor.execute(self, query, params)
130OperationalError: no such table: blogengine_tag
131
132======================================================================
133ERROR: test_delete_tag (blogengine.tests.AdminTest)
134----------------------------------------------------------------------
135Traceback (most recent call last):
136 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 306, in test_delete_tag
137 tag.save()
138 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
139 super(Tag, self).save()
140 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
141 force_update=force_update, update_fields=update_fields)
142 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
143 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
144 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
145 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
146 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
147 using=using, raw=raw)
148 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
149 return insert_query(self.model, objs, fields, **kwargs)
150 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
151 return query.get_compiler(using=using).execute_sql(return_id)
152 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
153 cursor.execute(sql, params)
154 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
155 return self.cursor.execute(sql, params)
156 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
157 six.reraise(dj_exc_type, dj_exc_value, traceback)
158 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
159 return self.cursor.execute(sql, params)
160 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
161 return Database.Cursor.execute(self, query, params)
162OperationalError: no such table: blogengine_tag
163
164======================================================================
165ERROR: test_edit_post (blogengine.tests.AdminTest)
166----------------------------------------------------------------------
167Traceback (most recent call last):
168 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 377, in test_edit_post
169 tag.save()
170 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
171 super(Tag, self).save()
172 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
173 force_update=force_update, update_fields=update_fields)
174 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
175 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
176 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
177 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
178 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
179 using=using, raw=raw)
180 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
181 return insert_query(self.model, objs, fields, **kwargs)
182 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
183 return query.get_compiler(using=using).execute_sql(return_id)
184 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
185 cursor.execute(sql, params)
186 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
187 return self.cursor.execute(sql, params)
188 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
189 six.reraise(dj_exc_type, dj_exc_value, traceback)
190 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
191 return self.cursor.execute(sql, params)
192 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
193 return Database.Cursor.execute(self, query, params)
194OperationalError: no such table: blogengine_tag
195
196======================================================================
197ERROR: test_edit_tag (blogengine.tests.AdminTest)
198----------------------------------------------------------------------
199Traceback (most recent call last):
200 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 279, in test_edit_tag
201 tag.save()
202 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
203 super(Tag, self).save()
204 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
205 force_update=force_update, update_fields=update_fields)
206 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
207 updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
208 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
209 result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
210 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
211 using=using, raw=raw)
212 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
213 return insert_query(self.model, objs, fields, **kwargs)
214 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
215 return query.get_compiler(using=using).execute_sql(return_id)
216 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
217 cursor.execute(sql, params)
218 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
219 return self.cursor.execute(sql, params)
220 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
221 six.reraise(dj_exc_type, dj_exc_value, traceback)
222 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
223 return self.cursor.execute(sql, params)
224 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
225 return Database.Cursor.execute(self, query, params)
226OperationalError: no such table: blogengine_tag
227
228======================================================================
229FAIL: test_create_tag (blogengine.tests.AdminTest)
230----------------------------------------------------------------------
231Traceback (most recent call last):
232 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 256, in test_create_tag
233 self.assertEquals(response.status_code, 200)
234AssertionError: 404 != 200
235
236----------------------------------------------------------------------
237Ran 18 tests in 3.981s
238
239FAILED (failures=1, errors=7)
240Destroying test database for alias 'default'...

Again, we can easily see why they failed - the blogengine_tag table is not in place. So let's create and run our migrations to fix that:

1$ python manage.py schemamigration --auto blogengine
2$ python manage.py migrate

Now, we run our tests again:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3.....F..F..F......
4======================================================================
5FAIL: test_create_tag (blogengine.tests.AdminTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 256, in test_create_tag
9 self.assertEquals(response.status_code, 200)
10AssertionError: 404 != 200
11
12======================================================================
13FAIL: test_delete_tag (blogengine.tests.AdminTest)
14----------------------------------------------------------------------
15Traceback (most recent call last):
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 315, in test_delete_tag
17 self.assertEquals(response.status_code, 200)
18AssertionError: 404 != 200
19
20======================================================================
21FAIL: test_edit_tag (blogengine.tests.AdminTest)
22----------------------------------------------------------------------
23Traceback (most recent call last):
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 289, in test_edit_tag
25 self.assertEquals(response.status_code, 200)
26AssertionError: 404 != 200
27
28----------------------------------------------------------------------
29Ran 18 tests in 5.124s
30
31FAILED (failures=3)
32Destroying test database for alias 'default'...

We can't yet amend our tags in the admin, because we haven't registered them. So we do that next:

1import models
2from django.contrib import admin
3from django.contrib.auth.models import User
4
5class PostAdmin(admin.ModelAdmin):
6 prepopulated_fields = {"slug": ("title",)}
7 exclude = ('author',)
8
9 def save_model(self, request, obj, form, change):
10 obj.author = request.user
11 obj.save()
12
13admin.site.register(models.Category)
14admin.site.register(models.Tag)
15admin.site.register(models.Post, PostAdmin)

Now, if we run our tests, they should pass:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3..................
4----------------------------------------------------------------------
5Ran 18 tests in 5.444s
6
7OK
8Destroying test database for alias 'default'...

Time to commit our changes:

1$ git add blogengine/
2$ git commit -m 'Implemented tags'

Now, like with the categories beforehand, we want to be able to show the tags applied to a post at the base of it, and list all posts for a specific tag. So, first of all, we'll amend our PostViewTest class to check for the tags:

1class PostViewTest(BaseAcceptanceTest):
2 def test_index(self):
3 # Create the category
4 category = Category()
5 category.name = 'python'
6 category.description = 'The Python programming language'
7 category.save()
8
9 # Create the tag
10 tag = Tag()
11 tag.name = 'perl'
12 tag.description = 'The Perl programming language'
13 tag.save()
14
15 # Create the author
16 author = User.objects.create_user('testuser', 'user@example.com', 'password')
17 author.save()
18
19 # Create the site
20 site = Site()
21 site.name = 'example.com'
22 site.domain = 'example.com'
23 site.save()
24
25 # Create the post
26 post = Post()
27 post.title = 'My first post'
28 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
29 post.slug = 'my-first-post'
30 post.pub_date = timezone.now()
31 post.author = author
32 post.site = site
33 post.category = category
34 post.save()
35 post.tags.add(tag)
36
37 # Check new post saved
38 all_posts = Post.objects.all()
39 self.assertEquals(len(all_posts), 1)
40
41 # Fetch the index
42 response = self.client.get('/')
43 self.assertEquals(response.status_code, 200)
44
45 # Check the post title is in the response
46 self.assertTrue(post.title in response.content)
47
48 # Check the post text is in the response
49 self.assertTrue(markdown.markdown(post.text) in response.content)
50
51 # Check the post category is in the response
52 self.assertTrue(post.category.name in response.content)
53
54 # Check the post tag is in the response
55 post_tag = all_posts[0].tags.all()[0]
56 self.assertTrue(post_tag.name in response.content)
57
58 # Check the post date is in the response
59 self.assertTrue(str(post.pub_date.year) in response.content)
60 self.assertTrue(post.pub_date.strftime('%b') in response.content)
61 self.assertTrue(str(post.pub_date.day) in response.content)
62
63 # Check the link is marked up properly
64 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
65
66 def test_post_page(self):
67 # Create the category
68 category = Category()
69 category.name = 'python'
70 category.description = 'The Python programming language'
71 category.save()
72
73 # Create the tag
74 tag = Tag()
75 tag.name = 'perl'
76 tag.description = 'The Perl programming language'
77 tag.save()
78
79 # Create the author
80 author = User.objects.create_user('testuser', 'user@example.com', 'password')
81 author.save()
82
83 # Create the site
84 site = Site()
85 site.name = 'example.com'
86 site.domain = 'example.com'
87 site.save()
88
89 # Create the post
90 post = Post()
91 post.title = 'My first post'
92 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
93 post.slug = 'my-first-post'
94 post.pub_date = timezone.now()
95 post.author = author
96 post.site = site
97 post.category = category
98 post.save()
99 post.tags.add(tag)
100 post.save()
101
102 # Check new post saved
103 all_posts = Post.objects.all()
104 self.assertEquals(len(all_posts), 1)
105 only_post = all_posts[0]
106 self.assertEquals(only_post, post)
107
108 # Get the post URL
109 post_url = only_post.get_absolute_url()
110
111 # Fetch the post
112 response = self.client.get(post_url)
113 self.assertEquals(response.status_code, 200)
114
115 # Check the post title is in the response
116 self.assertTrue(post.title in response.content)
117
118 # Check the post category is in the response
119 self.assertTrue(post.category.name in response.content)
120
121 # Check the post tag is in the response
122 post_tag = all_posts[0].tags.all()[0]
123 self.assertTrue(post_tag.name in response.content)
124
125 # Check the post text is in the response
126 self.assertTrue(markdown.markdown(post.text) in response.content)
127
128 # Check the post date is in the response
129 self.assertTrue(str(post.pub_date.year) in response.content)
130 self.assertTrue(post.pub_date.strftime('%b') in response.content)
131 self.assertTrue(str(post.pub_date.day) in response.content)
132
133 # Check the link is marked up properly
134 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

We create a tag near the top, and check for the text in the page (note that to avoid false positives from the categories, we set the name of the tags to something different). We do this on both the index and post pages.

We also need to put a test in place for the tag-specific page:

1 def test_tag_page(self):
2 # Create the tag
3 tag = Tag()
4 tag.name = 'python'
5 tag.description = 'The Python programming language'
6 tag.save()
7
8 # Create the author
9 author = User.objects.create_user('testuser', 'user@example.com', 'password')
10 author.save()
11
12 # Create the site
13 site = Site()
14 site.name = 'example.com'
15 site.domain = 'example.com'
16 site.save()
17
18 # Create the post
19 post = Post()
20 post.title = 'My first post'
21 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
22 post.slug = 'my-first-post'
23 post.pub_date = timezone.now()
24 post.author = author
25 post.site = site
26 post.save()
27 post.tags.add(tag)
28
29 # Check new post saved
30 all_posts = Post.objects.all()
31 self.assertEquals(len(all_posts), 1)
32 only_post = all_posts[0]
33 self.assertEquals(only_post, post)
34
35 # Get the tag URL
36 tag_url = post.tags.all()[0].get_absolute_url()
37
38 # Fetch the tag
39 response = self.client.get(tag_url)
40 self.assertEquals(response.status_code, 200)
41
42 # Check the tag name is in the response
43 self.assertTrue(post.tags.all()[0].name in response.content)
44
45 # Check the post text is in the response
46 self.assertTrue(markdown.markdown(post.text) in response.content)
47
48 # Check the post date is in the response
49 self.assertTrue(str(post.pub_date.year) in response.content)
50 self.assertTrue(post.pub_date.strftime('%b') in response.content)
51 self.assertTrue(str(post.pub_date.day) in response.content)
52
53 # Check the link is marked up properly
54 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

Again, this is virtually identical to the category page, adjusted to allow for the fact that we need to get a specific tag. If we now run our tests, they should fail as expected:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3................FFF
4======================================================================
5FAIL: test_index (blogengine.tests.PostViewTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 540, in test_index
9 self.assertTrue(post_tag.name in response.content)
10AssertionError: False is not true
11
12======================================================================
13FAIL: test_post_page (blogengine.tests.PostViewTest)
14----------------------------------------------------------------------
15Traceback (most recent call last):
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 607, in test_post_page
17 self.assertTrue(post_tag.name in response.content)
18AssertionError: False is not true
19
20======================================================================
21FAIL: test_tag_page (blogengine.tests.PostViewTest)
22----------------------------------------------------------------------
23Traceback (most recent call last):
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 714, in test_tag_page
25 self.assertEquals(response.status_code, 200)
26AssertionError: 404 != 200
27
28----------------------------------------------------------------------
29Ran 19 tests in 5.375s
30
31FAILED (failures=3)
32Destroying test database for alias 'default'...

So, we need to implement the following things:

  • Show tags on the index page
  • Show tags on the post pages
  • Create a page listing the posts with a specific tag

As we have seen already with the categories, this is actually quite simple. First, we'll sort out the tags on the index page:

1{% extends "blogengine/includes/base.html" %}
2
3 {% load custom_markdown %}
4
5 {% block content %}
6 {% for post in object_list %}
7 <div class="post">
8 <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
9 <h3>{{ post.pub_date }}</h3>
10 {{ post.text|custom_markdown }}
11 </div>
12 <a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a>
13 {% for tag in post.tags.all %}
14 <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
15 {% endfor %}
16 {% endfor %}
17
18 {% if page_obj.has_previous %}
19 <a href="/{{ page_obj.previous_page_number }}/">Previous Page</a>
20 {% endif %}
21 {% if page_obj.has_next %}
22 <a href="/{{ page_obj.next_page_number }}/">Next Page</a>
23 {% endif %}
24
25 {% endblock %}

This is quite simple. We retrieve all the tags with post.tags.all and loop through them. We then do basically the same for the individual post pages:

1{% extends "blogengine/includes/base.html" %}
2
3 {% load custom_markdown %}
4
5 {% block content %}
6 <div class="post">
7 <h1>{{ object.title }}</h1>
8 <h3>{{ object.pub_date }}</h3>
9 {{ object.text|custom_markdown }}
10 <a href="{{ object.category.get_absolute_url }}">{{ object.category.name }}</a>
11 {% for tag in post.tags.all %}
12 <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
13 {% endfor %}
14
15 <h4>Comments</h4>
16 <div class="fb-comments" data-href="http://{{ post.site }}{{ post.get_absolute_url }}" data-width="470" data-num-posts="10"></div>
17
18 </div>
19
20 {% endblock %}
21

This should resolve two of our outstanding tests:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3..................F
4======================================================================
5FAIL: test_tag_page (blogengine.tests.PostViewTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 714, in test_tag_page
9 self.assertEquals(response.status_code, 200)
10AssertionError: 404 != 200
11
12----------------------------------------------------------------------
13Ran 19 tests in 5.440s
14
15FAILED (failures=1)
16Destroying test database for alias 'default'...

The final test is for the tag pages. As we saw with the categories, we can limit our querysets on specific pages. So we'll extend the ListView generic view again to handle tags:

1from django.shortcuts import render
2from django.views.generic import ListView
3from blogengine.models import Category, Post, Tag
4
5# Create your views here.
6class CategoryListView(ListView):
7 def get_queryset(self):
8 slug = self.kwargs['slug']
9 try:
10 category = Category.objects.get(slug=slug)
11 return Post.objects.filter(category=category)
12 except Category.DoesNotExist:
13 return Post.objects.none()
14
15
16class TagListView(ListView):
17 def get_queryset(self):
18 slug = self.kwargs['slug']
19 try:
20 tag = Tag.objects.get(slug=slug)
21 return tag.post_set.all()
22 except Tag.DoesNotExist:
23 return Post.objects.none()

Note that here, Tag objects have access to their assigned Post objects - we just use post_set to refer to them and get all of the posts associated with that tag. Next we'll add the URLconfs:

1from django.conf.urls import patterns, url
2from django.views.generic import ListView, DetailView
3from blogengine.models import Post, Category, Tag
4from blogengine.views import CategoryListView, TagListView
5
6urlpatterns = patterns('',
7 # Index
8 url(r'^(?P<page>\d+)?/?$', ListView.as_view(
9 model=Post,
10 paginate_by=5,
11 )),
12
13 # Individual posts
14 url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
15 model=Post,
16 )),
17
18 # Categories
19 url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
20 paginate_by=5,
21 model=Category,
22 )),
23
24 # Tags
25 url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
26 paginate_by=5,
27 model=Tag,
28 )),
29)

We import the Tag model and the TagListView view, and use them to set up the tag page.

If we now run our tests again, they should pass:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3...................
4----------------------------------------------------------------------
5Ran 19 tests in 5.473s
6
7OK
8Destroying test database for alias 'default'...

Well done! Time to commit:

1$ git add templates/ blogengine/
2$ git commit -m 'Tags are now shown'

RSS Feed

For the final task today, we'll implement an RSS feed for our posts. Django ships with a handy syndication framework that makes it easy to implement this kind of functionality.

As usual, we'll create some tests first. In this case, we won't be adding any new models, so we don't need to test them. Instead we can jump straight into creating acceptance tests for our feed. For now we'll just create one type of feed: a feed of all the blog posts. In a later instalment we'll add feeds for categories and tags.

First of all, we'll implement our test. Now, in order to test our feed, we need to have a solution in place for parsing an RSS feed. Django won't do this natively, so we'll install the feedparser Python module. Run the following commands:

1$ pip install feedparser
2$ pip freeze > requirements.txt

Once that's done, feedparser should be available. You may wish to refer to the documentation as we go to help.

Let's write our test for the RSS feed. First, we import feedparser near the top of the file:

import feedparser

Then we define a new class for our feed tests. Put this towards the end of the file - I put it just before the flat page tests:

1class FeedTest(BaseAcceptanceTest):
2 def test_all_post_feed(self):
3 # Create the category
4 category = Category()
5 category.name = 'python'
6 category.description = 'The Python programming language'
7 category.save()
8
9 # Create the tag
10 tag = Tag()
11 tag.name = 'python'
12 tag.description = 'The Python programming language'
13 tag.save()
14
15 # Create the author
16 author = User.objects.create_user('testuser', 'user@example.com', 'password')
17 author.save()
18
19 # Create the site
20 site = Site()
21 site.name = 'example.com'
22 site.domain = 'example.com'
23 site.save()
24
25 # Create a post
26 post = Post()
27 post.title = 'My first post'
28 post.text = 'This is my first blog post'
29 post.slug = 'my-first-post'
30 post.pub_date = timezone.now()
31 post.author = author
32 post.site = site
33 post.category = category
34
35 # Save it
36 post.save()
37
38 # Add the tag
39 post.tags.add(tag)
40 post.save()
41
42 # Check we can find it
43 all_posts = Post.objects.all()
44 self.assertEquals(len(all_posts), 1)
45 only_post = all_posts[0]
46 self.assertEquals(only_post, post)
47
48 # Fetch the feed
49 response = self.client.get('/feeds/posts/')
50 self.assertEquals(response.status_code, 200)
51
52 # Parse the feed
53 feed = feedparser.parse(response.content)
54
55 # Check length
56 self.assertEquals(len(feed.entries), 1)
57
58 # Check post retrieved is the correct one
59 feed_post = feed.entries[0]
60 self.assertEquals(feed_post.title, post.title)
61 self.assertEquals(feed_post.description, post.text)

Run the tests and you'll see something like this:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3..............F.....
4======================================================================
5FAIL: test_all_post_feed (blogengine.tests.FeedTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 781, in test_all_post_feed
9 self.assertEquals(response.status_code, 200)
10AssertionError: 404 != 200
11
12----------------------------------------------------------------------
13Ran 20 tests in 6.743s
14
15FAILED (failures=1)
16Destroying test database for alias 'default'...

We're getting a 404 error because the post feed isn't implemented. So let's implement it. We're going to use Django's syndication framework, which will make it easy, but we need to enable it. Open up django_tutorial_blog_ng/settings/py and add the following under INSTALLED_APPS:

'django.contrib.syndication',

Next, we need to enable the URLconf for this RSS feed. Open up blogengine/urls.py and amend the import from blogengine.views` near the top:

from blogengine.views import CategoryListView, TagListView, PostsFeed

Further down, add in the following code to define the URL for the feed:

1 # Post RSS feed
2 url(r'^feeds/posts/$', PostsFeed()),

Note that we imported the PostsFeed class, but that hasn't yet been defined. So we need to do that. First of all, add this line near the top:

from django.contrib.syndication.views import Feed

This imports the syndication views - yes, they're another generic view! Our PostsFeed class is going to inherit from Feed. Next, we define the class:

1class PostsFeed(Feed):
2 title = "RSS feed - posts"
3 link = "feeds/posts/"
4 description = "RSS feed - blog posts"
5
6 def items(self):
7 return Post.objects.order_by('-pub_date')
8
9 def item_title(self, item):
10 return item.title
11
12 def item_description(self, item):
13 return item.text

This is fairly straightforward. We define our title, link, and description for the feed inside the class definition. We define the items method which sets what items are returned by the RSS feed. We also define the item_title and item_description methods.

Now, if we run our tests, they should pass:

1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
2Creating test database for alias 'default'...
3....................
4----------------------------------------------------------------------
5Ran 20 tests in 5.933s
6
7OK
8Destroying test database for alias 'default'...

Let's commit our changes:

1$ git add blogengine/ django_tutorial_blog_ng/ requirements.txt
2$ git commit -m 'RSS feed implemented'

And that's enough for now. Don't forget, you can get the code for this lesson by cloning the repository from Github and running git checkout lesson-4 to switch to this lesson.

Next time we'll:

  • Implement a search system
  • Add feeds for categories and posts
  • Tidy up the user interface

Hope to see you then!