Django blog tutorial - the next generation - part 4
Published by Matthew Daly 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 category3 category = Category()45 # Add attributes6 category.name = 'python'7 category.description = 'The Python programming language'89 # Save it10 category.save()1112 # Check we can find it13 all_categories = Category.objects.all()14 self.assertEquals(len(all_categories), 1)15 only_category = all_categories[0]16 self.assertEquals(only_category, category)1718 # Check attributes19 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 category3 category = Category()4 category.name = 'python'5 category.description = 'The Python programming language'6 category.save()78 # Create the author9 author = User.objects.create_user('testuser', 'user@example.com', 'password')10 author.save()1112 # Create the site13 site = Site()14 site.name = 'example.com'15 site.domain = 'example.com'16 site.save()1718 # Create the post19 post = Post()2021 # Set the attributes22 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 = author27 post.site = site28 post.category = category2930 # Save it31 post.save()3233 # Check we can find it34 all_posts = Post.objects.all()35 self.assertEquals(len(all_posts), 1)36 only_post = all_posts[0]37 self.assertEquals(only_post, post)3839 # Check attributes40 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 in3 self.client.login(username='bobsmith', password="password")45 # Check response code6 response = self.client.get('/admin/blogengine/category/add/')7 self.assertEquals(response.status_code, 200)89 # Create the new category10 response = self.client.post('/admin/blogengine/category/add/', {11 'name': 'python',12 'description': 'The Python programming language'13 },14 follow=True15 )16 self.assertEquals(response.status_code, 200)1718 # Check added successfully19 self.assertTrue('added successfully' in response.content)2021 # Check new category now in database22 all_categories = Category.objects.all()23 self.assertEquals(len(all_categories), 1)2425 def test_edit_category(self):26 # Create the category27 category = Category()28 category.name = 'python'29 category.description = 'The Python programming language'30 category.save()3132 # Log in33 self.client.login(username='bobsmith', password="password")3435 # Edit the category36 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)4142 # Check changed successfully43 self.assertTrue('changed successfully' in response.content)4445 # Check category amended46 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')5152 def test_delete_category(self):53 # Create the category54 category = Category()55 category.name = 'python'56 category.description = 'The Python programming language'57 category.save()5859 # Log in60 self.client.login(username='bobsmith', password="password")6162 # Delete the category63 response = self.client.post('/admin/blogengine/category/1/delete/', {64 'post': 'yes'65 }, follow=True)66 self.assertEquals(response.status_code, 200)6768 # Check deleted successfully69 self.assertTrue('deleted successfully' in response.content)7071 # Check category deleted72 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 category3 category = Category()4 category.name = 'python'5 category.description = 'The Python programming language'6 category.save()78 # Log in9 self.client.login(username='bobsmith', password="password")1011 # Check response code12 response = self.client.get('/admin/blogengine/post/add/')13 self.assertEquals(response.status_code, 200)1415 # Create the new post16 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=True26 )27 self.assertEquals(response.status_code, 200)2829 # Check added successfully30 self.assertTrue('added successfully' in response.content)3132 # Check new post now in database33 all_posts = Post.objects.all()34 self.assertEquals(len(all_posts), 1)3536 def test_edit_post(self):37 # Create the category38 category = Category()39 category.name = 'python'40 category.description = 'The Python programming language'41 category.save()4243 # Create the author44 author = User.objects.create_user('testuser', 'user@example.com', 'password')45 author.save()4647 # Create the site48 site = Site()49 site.name = 'example.com'50 site.domain = 'example.com'51 site.save()5253 # Create the post54 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 = author60 post.site = site61 post.save()6263 # Log in64 self.client.login(username='bobsmith', password="password")6566 # Edit the post67 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=True77 )78 self.assertEquals(response.status_code, 200)7980 # Check changed successfully81 self.assertTrue('changed successfully' in response.content)8283 # Check post amended84 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')8990 def test_delete_post(self):91 # Create the category92 category = Category()93 category.name = 'python'94 category.description = 'The Python programming language'95 category.save()9697 # Create the author98 author = User.objects.create_user('testuser', 'user@example.com', 'password')99 author.save()100101 # Create the site102 site = Site()103 site.name = 'example.com'104 site.domain = 'example.com'105 site.save()106107 # Create the post108 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 = site114 post.author = author115 post.category = category116 post.save()117118 # Check new post saved119 all_posts = Post.objects.all()120 self.assertEquals(len(all_posts), 1)121122 # Log in123 self.client.login(username='bobsmith', password="password")124125 # Delete the post126 response = self.client.post('/admin/blogengine/post/1/delete/', {127 'post': 'yes'128 }, follow=True)129 self.assertEquals(response.status_code, 200)130131 # Check deleted successfully132 self.assertTrue('deleted successfully' in response.content)133134 # Check post deleted135 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 category4 category = Category()5 category.name = 'python'6 category.description = 'The Python programming language'7 category.save()89 # Create the author10 author = User.objects.create_user('testuser', 'user@example.com', 'password')11 author.save()1213 # Create the site14 site = Site()15 site.name = 'example.com'16 site.domain = 'example.com'17 site.save()1819 # Create the post20 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 = author26 post.site = site27 post.category = category28 post.save()2930 # Check new post saved31 all_posts = Post.objects.all()32 self.assertEquals(len(all_posts), 1)3334 # Fetch the index35 response = self.client.get('/')36 self.assertEquals(response.status_code, 200)3738 # Check the post title is in the response39 self.assertTrue(post.title in response.content)4041 # Check the post text is in the response42 self.assertTrue(markdown.markdown(post.text) in response.content)4344 # Check the post date is in the response45 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)4849 # Check the link is marked up properly50 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)5152 def test_post_page(self):53 # Create the category54 category = Category()55 category.name = 'python'56 category.description = 'The Python programming language'57 category.save()5859 # Create the author60 author = User.objects.create_user('testuser', 'user@example.com', 'password')61 author.save()6263 # Create the site64 site = Site()65 site.name = 'example.com'66 site.domain = 'example.com'67 site.save()6869 # Create the post70 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 = author76 post.site = site77 post.category = category78 post.save()7980 # Check new post saved81 all_posts = Post.objects.all()82 self.assertEquals(len(all_posts), 1)83 only_post = all_posts[0]84 self.assertEquals(only_post, post)8586 # Get the post URL87 post_url = only_post.get_absolute_url()8889 # Fetch the post90 response = self.client.get(post_url)91 self.assertEquals(response.status_code, 200)9293 # Check the post title is in the response94 self.assertTrue(post.title in response.content)9596 # Check the post text is in the response97 self.assertTrue(markdown.markdown(post.text) in response.content)9899 # Check the post date is in the response100 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)103104 # Check the link is marked up properly105 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 blogengine2Creating test database for alias 'default'...3E4======================================================================5ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)6----------------------------------------------------------------------7ImportError: Failed to import test module: blogengine.tests8Traceback (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_tests10 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_name12 __import__(name)13 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 3, in <module>14 from blogengine.models import Post, Category15ImportError: cannot import name Category161718----------------------------------------------------------------------19Ran 1 test in 0.000s2021FAILED (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 models2from django.contrib.auth.models import User3from django.contrib.sites.models import Site45# Create your models here.6class Category(models.Model):7 name = models.CharField(max_length=200)8 description = models.TextField()91011class 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)1920 def get_absolute_url(self):21 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)2223 def __unicode__(self):24 return self.title2526 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 blogengine2Creating test database for alias 'default'...3EEFEEEEE...EE4======================================================================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_category9 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 save11 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_base13 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_table15 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_insert17 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 _insert19 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_query21 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_sql23 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 execute25 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 execute29 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 execute31 return Database.Cursor.execute(self, query, params)32OperationalError: no such table: blogengine_category3334======================================================================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_post39 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 save41 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_base43 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_table45 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_insert47 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 _insert49 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_query51 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_sql53 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 execute55 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 execute59 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 execute61 return Database.Cursor.execute(self, query, params)62OperationalError: no such table: blogengine_category6364======================================================================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_post69 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 save71 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_base73 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_table75 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_insert77 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 _insert79 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_query81 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_sql83 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 execute85 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 execute89 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 execute91 return Database.Cursor.execute(self, query, params)92OperationalError: no such table: blogengine_category9394======================================================================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_category99 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 save101 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_base103 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_table105 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_insert107 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 _insert109 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_query111 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_sql113 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 execute115 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 execute119 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 execute121 return Database.Cursor.execute(self, query, params)122OperationalError: no such table: blogengine_category123124======================================================================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_post129 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 save131 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_base133 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_table135 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_insert137 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 _insert139 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_query141 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_sql143 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 execute145 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 execute149 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 execute151 return Database.Cursor.execute(self, query, params)152OperationalError: no such table: blogengine_category153154======================================================================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_category159 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 save161 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_base163 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_table165 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_insert167 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 _insert169 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_query171 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_sql173 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 execute175 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 execute179 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 execute181 return Database.Cursor.execute(self, query, params)182OperationalError: no such table: blogengine_category183184======================================================================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_post189 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 save191 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_base193 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_table195 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_insert197 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 _insert199 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_query201 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_sql203 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 execute205 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 execute209 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 execute211 return Database.Cursor.execute(self, query, params)212OperationalError: no such table: blogengine_category213214======================================================================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_index219 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 save221 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_base223 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_table225 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_insert227 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 _insert229 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_query231 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_sql233 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 execute235 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 execute239 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 execute241 return Database.Cursor.execute(self, query, params)242OperationalError: no such table: blogengine_category243244======================================================================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_page249 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 save251 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_base253 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_table255 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_insert257 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 _insert259 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_query261 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_sql263 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 execute265 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 execute269 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 execute271 return Database.Cursor.execute(self, query, params)272OperationalError: no such table: blogengine_category273274======================================================================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_category279 self.assertEquals(response.status_code, 200)280AssertionError: 404 != 200281282----------------------------------------------------------------------283Ran 13 tests in 3.393s284285FAILED (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 blogengine2$ 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_category8 self.assertEquals(response.status_code, 200)9AssertionError: 404 != 2001011======================================================================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_category16 self.assertEquals(response.status_code, 200)17AssertionError: 404 != 2001819======================================================================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_category24 self.assertEquals(response.status_code, 200)25AssertionError: 404 != 2002627----------------------------------------------------------------------28Ran 13 tests in 4.047s2930FAILED (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 models2from django.contrib import admin3from django.contrib.auth.models import User45class PostAdmin(admin.ModelAdmin):6 prepopulated_fields = {"slug": ("title",)}7 exclude = ('author',)89 def save_model(self, request, obj, form, change):10 obj.author = request.user11 obj.save()1213admin.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 blogengine2Creating test database for alias 'default'...3.............4----------------------------------------------------------------------5Ran 13 tests in 4.092s67OK8Destroying 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 models2from django.contrib.auth.models import User3from django.contrib.sites.models import Site45# Create your models here.6class Category(models.Model):7 name = models.CharField(max_length=200)8 description = models.TextField()910 def __unicode__(self):11 return self.name1213 class Meta:14 verbose_name_plural = 'categories'151617class 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)2526 def get_absolute_url(self):27 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)2829 def __unicode__(self):30 return self.title3132 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 category4 category = Category()5 category.name = 'python'6 category.description = 'The Python programming language'7 category.save()89 # Create the author10 author = User.objects.create_user('testuser', 'user@example.com', 'password')11 author.save()1213 # Create the site14 site = Site()15 site.name = 'example.com'16 site.domain = 'example.com'17 site.save()1819 # Create the post20 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 = author26 post.site = site27 post.category = category28 post.save()2930 # Check new post saved31 all_posts = Post.objects.all()32 self.assertEquals(len(all_posts), 1)3334 # Fetch the index35 response = self.client.get('/')36 self.assertEquals(response.status_code, 200)3738 # Check the post title is in the response39 self.assertTrue(post.title in response.content)4041 # Check the post text is in the response42 self.assertTrue(markdown.markdown(post.text) in response.content)4344 # Check the post category is in the response45 self.assertTrue(post.category.name in response.content)4647 # Check the post date is in the response48 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)5152 # Check the link is marked up properly53 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)5455 def test_post_page(self):56 # Create the category57 category = Category()58 category.name = 'python'59 category.description = 'The Python programming language'60 category.save()6162 # Create the author63 author = User.objects.create_user('testuser', 'user@example.com', 'password')64 author.save()6566 # Create the site67 site = Site()68 site.name = 'example.com'69 site.domain = 'example.com'70 site.save()7172 # Create the post73 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 = author79 post.site = site80 post.category = category81 post.save()8283 # Check new post saved84 all_posts = Post.objects.all()85 self.assertEquals(len(all_posts), 1)86 only_post = all_posts[0]87 self.assertEquals(only_post, post)8889 # Get the post URL90 post_url = only_post.get_absolute_url()9192 # Fetch the post93 response = self.client.get(post_url)94 self.assertEquals(response.status_code, 200)9596 # Check the post title is in the response97 self.assertTrue(post.title in response.content)9899 # Check the post category is in the response100 self.assertTrue(post.category.name in response.content)101102 # Check the post text is in the response103 self.assertTrue(markdown.markdown(post.text) in response.content)104105 # Check the post date is in the response106 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)109110 # Check the link is marked up properly111 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 category3 category = Category()4 category.name = 'python'5 category.description = 'The Python programming language'6 category.save()78 # Create the author9 author = User.objects.create_user('testuser', 'user@example.com', 'password')10 author.save()1112 # Create the site13 site = Site()14 site.name = 'example.com'15 site.domain = 'example.com'16 site.save()1718 # Create the post19 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 = author25 post.site = site26 post.category = category27 post.save()2829 # Check new post saved30 all_posts = Post.objects.all()31 self.assertEquals(len(all_posts), 1)32 only_post = all_posts[0]33 self.assertEquals(only_post, post)3435 # Get the category URL36 category_url = post.category.get_absolute_url()3738 # Fetch the category39 response = self.client.get(category_url)40 self.assertEquals(response.status_code, 200)4142 # Check the category name is in the response43 self.assertTrue(post.category.name in response.content)4445 # Check the post text is in the response46 self.assertTrue(markdown.markdown(post.text) in response.content)4748 # Check the post date is in the response49 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)5253 # Check the link is marked up properly54 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 blogengine2Creating test database for alias 'default'...3...........EFF4======================================================================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_page9 category_url = post.category.get_absolute_url()10AttributeError: 'Category' object has no attribute 'get_absolute_url'1112======================================================================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_index17 self.assertTrue(post.category.name in response.content)18AssertionError: False is not true1920======================================================================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_page25 self.assertTrue(post.category.name in response.content)26AssertionError: False is not true2728----------------------------------------------------------------------29Ran 14 tests in 5.017s3031FAILED (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 models2from django.contrib.auth.models import User3from django.contrib.sites.models import Site4from django.utils.text import slugify56# 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)1112 def save(self):13 if not self.slug:14 self.slug = slugify(unicode(self.name))15 super(Category, self).save()1617 def get_absolute_url(self):18 return "/category/%s/" % (self.slug)1920 def __unicode__(self):21 return self.name2223 class Meta:24 verbose_name_plural = 'categories'252627class 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)3536 def get_absolute_url(self):37 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)3839 def __unicode__(self):40 return self.title4142 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" %}23 {% load custom_markdown %}45 {% 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 %}1415 {% 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 %}2122 {% endblock %}
Next, we'll take care of our post detail page:
1{% extends "blogengine/includes/base.html" %}23 {% load custom_markdown %}45 {% 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>1112 <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>1415 </div>1617 {% 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, url2from django.views.generic import ListView, DetailView3from blogengine.models import Post, Category4from blogengine.views import CategoryListView56urlpatterns = patterns('',7 # Index8 url(r'^(?P<page>\d+)?/?$', ListView.as_view(9 model=Post,10 paginate_by=5,11 )),1213 # Individual posts14 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 )),1718 # Categories19 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 render2from django.views.generic import ListView3from blogengine.models import Category, Post45# 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.083s56OK7Destroying 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 tag3 tag = Tag()45 # Add attributes6 tag.name = 'python'7 tag.description = 'The Python programming language'89 # Save it10 tag.save()1112 # Check we can find it13 all_tags = Tag.objects.all()14 self.assertEquals(len(all_tags), 1)15 only_tag = all_tags[0]16 self.assertEquals(only_tag, tag)1718 # Check attributes19 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 category3 category = Category()4 category.name = 'python'5 category.description = 'The Python programming language'6 category.save()78 # Create the tag9 tag = Tag()10 tag.name = 'python'11 tag.description = 'The Python programming language'12 tag.save()1314 # Create the author15 author = User.objects.create_user('testuser', 'user@example.com', 'password')16 author.save()1718 # Create the site19 site = Site()20 site.name = 'example.com'21 site.domain = 'example.com'22 site.save()2324 # Create the post25 post = Post()2627 # Set the attributes28 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 = author33 post.site = site34 post.category = category3536 # Save it37 post.save()3839 # Add the tag40 post.tags.add(tag)41 post.save()4243 # Check we can find it44 all_posts = Post.objects.all()45 self.assertEquals(len(all_posts), 1)46 only_post = all_posts[0]47 self.assertEquals(only_post, post)4849 # Check attributes50 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')6566 # Check tags67 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 in3 self.client.login(username='bobsmith', password="password")45 # Check response code6 response = self.client.get('/admin/blogengine/tag/add/')7 self.assertEquals(response.status_code, 200)89 # Create the new tag10 response = self.client.post('/admin/blogengine/tag/add/', {11 'name': 'python',12 'description': 'The Python programming language'13 },14 follow=True15 )16 self.assertEquals(response.status_code, 200)1718 # Check added successfully19 self.assertTrue('added successfully' in response.content)2021 # Check new tag now in database22 all_tags = Tag.objects.all()23 self.assertEquals(len(all_tags), 1)2425 def test_edit_tag(self):26 # Create the tag27 tag = Tag()28 tag.name = 'python'29 tag.description = 'The Python programming language'30 tag.save()3132 # Log in33 self.client.login(username='bobsmith', password="password")3435 # Edit the tag36 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)4142 # Check changed successfully43 self.assertTrue('changed successfully' in response.content)4445 # Check tag amended46 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')5152 def test_delete_tag(self):53 # Create the tag54 tag = Tag()55 tag.name = 'python'56 tag.description = 'The Python programming language'57 tag.save()5859 # Log in60 self.client.login(username='bobsmith', password="password")6162 # Delete the tag63 response = self.client.post('/admin/blogengine/tag/1/delete/', {64 'post': 'yes'65 }, follow=True)66 self.assertEquals(response.status_code, 200)6768 # Check deleted successfully69 self.assertTrue('deleted successfully' in response.content)7071 # Check tag deleted72 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 category3 category = Category()4 category.name = 'python'5 category.description = 'The Python programming language'6 category.save()78 # Create the tag9 tag = Tag()10 tag.name = 'python'11 tag.description = 'The Python programming language'12 tag.save()1314 # Log in15 self.client.login(username='bobsmith', password="password")1617 # Check response code18 response = self.client.get('/admin/blogengine/post/add/')19 self.assertEquals(response.status_code, 200)2021 # Create the new post22 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=True33 )34 self.assertEquals(response.status_code, 200)3536 # Check added successfully37 self.assertTrue('added successfully' in response.content)3839 # Check new post now in database40 all_posts = Post.objects.all()41 self.assertEquals(len(all_posts), 1)4243 def test_edit_post(self):44 # Create the category45 category = Category()46 category.name = 'python'47 category.description = 'The Python programming language'48 category.save()4950 # Create the tag51 tag = Tag()52 tag.name = 'python'53 tag.description = 'The Python programming language'54 tag.save()5556 # Create the author57 author = User.objects.create_user('testuser', 'user@example.com', 'password')58 author.save()5960 # Create the site61 site = Site()62 site.name = 'example.com'63 site.domain = 'example.com'64 site.save()6566 # Create the post67 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 = author73 post.site = site74 post.save()75 post.tags.add(tag)76 post.save()7778 # Log in79 self.client.login(username='bobsmith', password="password")8081 # Edit the post82 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=True93 )94 self.assertEquals(response.status_code, 200)9596 # Check changed successfully97 self.assertTrue('changed successfully' in response.content)9899 # Check post amended100 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')105106 def test_delete_post(self):107 # Create the category108 category = Category()109 category.name = 'python'110 category.description = 'The Python programming language'111 category.save()112113 # Create the tag114 tag = Tag()115 tag.name = 'python'116 tag.description = 'The Python programming language'117 tag.save()118119 # Create the author120 author = User.objects.create_user('testuser', 'user@example.com', 'password')121 author.save()122123 # Create the site124 site = Site()125 site.name = 'example.com'126 site.domain = 'example.com'127 site.save()128129 # Create the post130 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 = site136 post.author = author137 post.category = category138 post.save()139 post.tags.add(tag)140 post.save()141142 # Check new post saved143 all_posts = Post.objects.all()144 self.assertEquals(len(all_posts), 1)145146 # Log in147 self.client.login(username='bobsmith', password="password")148149 # Delete the post150 response = self.client.post('/admin/blogengine/post/1/delete/', {151 'post': 'yes'152 }, follow=True)153 self.assertEquals(response.status_code, 200)154155 # Check deleted successfully156 self.assertTrue('deleted successfully' in response.content)157158 # Check post deleted159 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 blogengine2Creating test database for alias 'default'...3E4======================================================================5ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)6----------------------------------------------------------------------7ImportError: Failed to import test module: blogengine.tests8Traceback (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_tests10 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_name12 __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, Tag15ImportError: cannot import name Tag161718----------------------------------------------------------------------19Ran 1 test in 0.000s2021FAILED (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)56 def save(self):7 if not self.slug:8 self.slug = slugify(unicode(self.name))9 super(Tag, self).save()1011 def get_absolute_url(self):12 return "/tag/%s/" % (self.slug)1314 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)1011 def get_absolute_url(self):12 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)1314 def __unicode__(self):15 return self.title1617 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 blogengine2Creating 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_post9 tag.save()10 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save11 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 save13 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_base15 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_table17 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_insert19 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 _insert21 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_query23 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_sql25 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 execute27 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 execute31 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 execute33 return Database.Cursor.execute(self, query, params)34OperationalError: no such table: blogengine_tag3536======================================================================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_tag41 tag.save()42 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save43 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 save45 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_base47 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_table49 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_insert51 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 _insert53 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_query55 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_sql57 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 execute59 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 execute63 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 execute65 return Database.Cursor.execute(self, query, params)66OperationalError: no such table: blogengine_tag6768======================================================================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_post73 tag.save()74 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save75 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 save77 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_base79 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_table81 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_insert83 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 _insert85 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_query87 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_sql89 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 execute91 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 execute95 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 execute97 return Database.Cursor.execute(self, query, params)98OperationalError: no such table: blogengine_tag99100======================================================================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_post105 tag.save()106 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save107 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 save109 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_base111 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_table113 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_insert115 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 _insert117 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_query119 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_sql121 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 execute123 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 execute127 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 execute129 return Database.Cursor.execute(self, query, params)130OperationalError: no such table: blogengine_tag131132======================================================================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_tag137 tag.save()138 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save139 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 save141 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_base143 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_table145 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_insert147 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 _insert149 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_query151 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_sql153 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 execute155 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 execute159 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 execute161 return Database.Cursor.execute(self, query, params)162OperationalError: no such table: blogengine_tag163164======================================================================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_post169 tag.save()170 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save171 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 save173 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_base175 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_table177 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_insert179 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 _insert181 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_query183 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_sql185 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 execute187 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 execute191 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 execute193 return Database.Cursor.execute(self, query, params)194OperationalError: no such table: blogengine_tag195196======================================================================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_tag201 tag.save()202 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save203 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 save205 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_base207 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_table209 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_insert211 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 _insert213 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_query215 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_sql217 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 execute219 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 execute223 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 execute225 return Database.Cursor.execute(self, query, params)226OperationalError: no such table: blogengine_tag227228======================================================================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_tag233 self.assertEquals(response.status_code, 200)234AssertionError: 404 != 200235236----------------------------------------------------------------------237Ran 18 tests in 3.981s238239FAILED (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 blogengine2$ python manage.py migrate
Now, we run our tests again:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine2Creating 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_tag9 self.assertEquals(response.status_code, 200)10AssertionError: 404 != 2001112======================================================================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_tag17 self.assertEquals(response.status_code, 200)18AssertionError: 404 != 2001920======================================================================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_tag25 self.assertEquals(response.status_code, 200)26AssertionError: 404 != 2002728----------------------------------------------------------------------29Ran 18 tests in 5.124s3031FAILED (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 models2from django.contrib import admin3from django.contrib.auth.models import User45class PostAdmin(admin.ModelAdmin):6 prepopulated_fields = {"slug": ("title",)}7 exclude = ('author',)89 def save_model(self, request, obj, form, change):10 obj.author = request.user11 obj.save()1213admin.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 blogengine2Creating test database for alias 'default'...3..................4----------------------------------------------------------------------5Ran 18 tests in 5.444s67OK8Destroying 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 category4 category = Category()5 category.name = 'python'6 category.description = 'The Python programming language'7 category.save()89 # Create the tag10 tag = Tag()11 tag.name = 'perl'12 tag.description = 'The Perl programming language'13 tag.save()1415 # Create the author16 author = User.objects.create_user('testuser', 'user@example.com', 'password')17 author.save()1819 # Create the site20 site = Site()21 site.name = 'example.com'22 site.domain = 'example.com'23 site.save()2425 # Create the post26 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 = author32 post.site = site33 post.category = category34 post.save()35 post.tags.add(tag)3637 # Check new post saved38 all_posts = Post.objects.all()39 self.assertEquals(len(all_posts), 1)4041 # Fetch the index42 response = self.client.get('/')43 self.assertEquals(response.status_code, 200)4445 # Check the post title is in the response46 self.assertTrue(post.title in response.content)4748 # Check the post text is in the response49 self.assertTrue(markdown.markdown(post.text) in response.content)5051 # Check the post category is in the response52 self.assertTrue(post.category.name in response.content)5354 # Check the post tag is in the response55 post_tag = all_posts[0].tags.all()[0]56 self.assertTrue(post_tag.name in response.content)5758 # Check the post date is in the response59 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)6263 # Check the link is marked up properly64 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)6566 def test_post_page(self):67 # Create the category68 category = Category()69 category.name = 'python'70 category.description = 'The Python programming language'71 category.save()7273 # Create the tag74 tag = Tag()75 tag.name = 'perl'76 tag.description = 'The Perl programming language'77 tag.save()7879 # Create the author80 author = User.objects.create_user('testuser', 'user@example.com', 'password')81 author.save()8283 # Create the site84 site = Site()85 site.name = 'example.com'86 site.domain = 'example.com'87 site.save()8889 # Create the post90 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 = author96 post.site = site97 post.category = category98 post.save()99 post.tags.add(tag)100 post.save()101102 # Check new post saved103 all_posts = Post.objects.all()104 self.assertEquals(len(all_posts), 1)105 only_post = all_posts[0]106 self.assertEquals(only_post, post)107108 # Get the post URL109 post_url = only_post.get_absolute_url()110111 # Fetch the post112 response = self.client.get(post_url)113 self.assertEquals(response.status_code, 200)114115 # Check the post title is in the response116 self.assertTrue(post.title in response.content)117118 # Check the post category is in the response119 self.assertTrue(post.category.name in response.content)120121 # Check the post tag is in the response122 post_tag = all_posts[0].tags.all()[0]123 self.assertTrue(post_tag.name in response.content)124125 # Check the post text is in the response126 self.assertTrue(markdown.markdown(post.text) in response.content)127128 # Check the post date is in the response129 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)132133 # Check the link is marked up properly134 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 tag3 tag = Tag()4 tag.name = 'python'5 tag.description = 'The Python programming language'6 tag.save()78 # Create the author9 author = User.objects.create_user('testuser', 'user@example.com', 'password')10 author.save()1112 # Create the site13 site = Site()14 site.name = 'example.com'15 site.domain = 'example.com'16 site.save()1718 # Create the post19 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 = author25 post.site = site26 post.save()27 post.tags.add(tag)2829 # Check new post saved30 all_posts = Post.objects.all()31 self.assertEquals(len(all_posts), 1)32 only_post = all_posts[0]33 self.assertEquals(only_post, post)3435 # Get the tag URL36 tag_url = post.tags.all()[0].get_absolute_url()3738 # Fetch the tag39 response = self.client.get(tag_url)40 self.assertEquals(response.status_code, 200)4142 # Check the tag name is in the response43 self.assertTrue(post.tags.all()[0].name in response.content)4445 # Check the post text is in the response46 self.assertTrue(markdown.markdown(post.text) in response.content)4748 # Check the post date is in the response49 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)5253 # Check the link is marked up properly54 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 blogengine2Creating test database for alias 'default'...3................FFF4======================================================================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_index9 self.assertTrue(post_tag.name in response.content)10AssertionError: False is not true1112======================================================================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_page17 self.assertTrue(post_tag.name in response.content)18AssertionError: False is not true1920======================================================================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_page25 self.assertEquals(response.status_code, 200)26AssertionError: 404 != 2002728----------------------------------------------------------------------29Ran 19 tests in 5.375s3031FAILED (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" %}23 {% load custom_markdown %}45 {% 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 %}1718 {% 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 %}2425 {% 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" %}23 {% load custom_markdown %}45 {% 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 %}1415 <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>1718 </div>1920 {% endblock %}21
This should resolve two of our outstanding tests:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine2Creating test database for alias 'default'...3..................F4======================================================================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_page9 self.assertEquals(response.status_code, 200)10AssertionError: 404 != 2001112----------------------------------------------------------------------13Ran 19 tests in 5.440s1415FAILED (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 render2from django.views.generic import ListView3from blogengine.models import Category, Post, Tag45# 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()141516class 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, url2from django.views.generic import ListView, DetailView3from blogengine.models import Post, Category, Tag4from blogengine.views import CategoryListView, TagListView56urlpatterns = patterns('',7 # Index8 url(r'^(?P<page>\d+)?/?$', ListView.as_view(9 model=Post,10 paginate_by=5,11 )),1213 # Individual posts14 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 )),1718 # Categories19 url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(20 paginate_by=5,21 model=Category,22 )),2324 # Tags25 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 blogengine2Creating test database for alias 'default'...3...................4----------------------------------------------------------------------5Ran 19 tests in 5.473s67OK8Destroying 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 feedparser2$ 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 category4 category = Category()5 category.name = 'python'6 category.description = 'The Python programming language'7 category.save()89 # Create the tag10 tag = Tag()11 tag.name = 'python'12 tag.description = 'The Python programming language'13 tag.save()1415 # Create the author16 author = User.objects.create_user('testuser', 'user@example.com', 'password')17 author.save()1819 # Create the site20 site = Site()21 site.name = 'example.com'22 site.domain = 'example.com'23 site.save()2425 # Create a post26 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 = author32 post.site = site33 post.category = category3435 # Save it36 post.save()3738 # Add the tag39 post.tags.add(tag)40 post.save()4142 # Check we can find it43 all_posts = Post.objects.all()44 self.assertEquals(len(all_posts), 1)45 only_post = all_posts[0]46 self.assertEquals(only_post, post)4748 # Fetch the feed49 response = self.client.get('/feeds/posts/')50 self.assertEquals(response.status_code, 200)5152 # Parse the feed53 feed = feedparser.parse(response.content)5455 # Check length56 self.assertEquals(len(feed.entries), 1)5758 # Check post retrieved is the correct one59 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 blogengine2Creating 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_feed9 self.assertEquals(response.status_code, 200)10AssertionError: 404 != 2001112----------------------------------------------------------------------13Ran 20 tests in 6.743s1415FAILED (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 feed2 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"56 def items(self):7 return Post.objects.order_by('-pub_date')89 def item_title(self, item):10 return item.title1112 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 blogengine2Creating test database for alias 'default'...3....................4----------------------------------------------------------------------5Ran 20 tests in 5.933s67OK8Destroying test database for alias 'default'...
Let's commit our changes:
1$ git add blogengine/ django_tutorial_blog_ng/ requirements.txt2$ 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!