Django Blog Tutorial - the Next Generation - Part 9

Published by at 28th September 2014 7:51 pm

Yes, I know the eight instalment was meant to be the last one! Within 24 hours of that post going live, Django 1.7 was released, so naturally I'd like to show you how to upgrade to it.

The biggest change is that Django 1.7 introduces its own migration system, which means South is now surplus to requirements. We therefore need to switch from South to Django's native migrations. Fortunately, this is fairly straightforward.

First of all, activate your virtualenv:

$ virtualenv venv

Then make sure your migrations are up to date:

1$ python manage.py syncdb
2$ python manage.py migrate

Then, upgrade your Django version and uninstall South:

1$ pip install Django --upgrade
2$ pip uninstall South
3$ pip freeze > requirements.txt

Next, remove South from INSTALLED_APPS in django_tutorial_blog_ng/settings.py.

You now need to delete all of the numbered migration files in blogengine/migrations/, and the relevant .pyc files, but NOT the directory or the __init__.py file. You can do so with this command on Linux or OS X:

$ rm blogengine/migrations/00*

Next, we recreate our migrations with the following command:

1$ python manage.py makemigrations
2Migrations for 'blogengine':
3 0001_initial.py:
4 - Create model Category
5 - Create model Post
6 - Create model Tag
7 - Add field tags to post

Then we run the migrations:

1$ python manage.py migrate
2Operations to perform:
3 Synchronize unmigrated apps: sitemaps, django_jenkins, debug_toolbar
4 Apply all migrations: sessions, admin, sites, flatpages, contenttypes, auth, blogengine
5Synchronizing apps without migrations:
6 Creating tables...
7 Installing custom SQL...
8 Installing indexes...
9Running migrations:
10 Applying contenttypes.0001_initial... FAKED
11 Applying auth.0001_initial... FAKED
12 Applying admin.0001_initial... FAKED
13 Applying sites.0001_initial... FAKED
14 Applying blogengine.0001_initial... FAKED
15 Applying flatpages.0001_initial... FAKED
16 Applying sessions.0001_initial... FAKED

Don't worry too much if the output doesn't look exactly the same as this - as long as it works, that's the main thing.

Let's run our test suite to ensure it works:

1$ python manage.py jenkins
2Creating test database for alias 'default'...
3....FF.F.FFFFFF..............
4======================================================================
5FAIL: test_create_post (blogengine.tests.AdminTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 385, in test_create_post
9 self.assertTrue('added successfully' in response.content)
10AssertionError: False is not true
11
12======================================================================
13FAIL: test_create_post_without_tag (blogengine.tests.AdminTest)
14----------------------------------------------------------------------
15Traceback (most recent call last):
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 417, in test_create_post_without_tag
17 self.assertTrue('added successfully' in response.content)
18AssertionError: False is not true
19
20======================================================================
21FAIL: test_delete_category (blogengine.tests.AdminTest)
22----------------------------------------------------------------------
23Traceback (most recent call last):
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 278, in test_delete_category
25 self.assertEquals(response.status_code, 200)
26AssertionError: 404 != 200
27
28======================================================================
29FAIL: test_delete_tag (blogengine.tests.AdminTest)
30----------------------------------------------------------------------
31Traceback (most recent call last):
32 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 346, in test_delete_tag
33 self.assertEquals(response.status_code, 200)
34AssertionError: 404 != 200
35
36======================================================================
37FAIL: test_edit_category (blogengine.tests.AdminTest)
38----------------------------------------------------------------------
39Traceback (most recent call last):
40 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 255, in test_edit_category
41 self.assertEquals(response.status_code, 200)
42AssertionError: 404 != 200
43
44======================================================================
45FAIL: test_edit_post (blogengine.tests.AdminTest)
46----------------------------------------------------------------------
47Traceback (most recent call last):
48 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 447, in test_edit_post
49 self.assertEquals(response.status_code, 200)
50AssertionError: 404 != 200
51
52======================================================================
53FAIL: test_edit_tag (blogengine.tests.AdminTest)
54----------------------------------------------------------------------
55Traceback (most recent call last):
56 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 323, in test_edit_tag
57 self.assertEquals(response.status_code, 200)
58AssertionError: 404 != 200
59
60======================================================================
61FAIL: test_login (blogengine.tests.AdminTest)
62----------------------------------------------------------------------
63Traceback (most recent call last):
64 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 183, in test_login
65 self.assertEquals(response.status_code, 200)
66AssertionError: 302 != 200
67
68======================================================================
69FAIL: test_logout (blogengine.tests.AdminTest)
70----------------------------------------------------------------------
71Traceback (most recent call last):
72 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 214, in test_logout
73 self.assertEquals(response.status_code, 200)
74AssertionError: 302 != 200
75
76----------------------------------------------------------------------
77Ran 29 tests in 7.383s
78
79FAILED (failures=9)
80Destroying test database for alias 'default'...

We have an issue here. A load of the tests for the admin interface now fail. If we now try running the dev server, we see this error:

1$ python manage.py runserver
2Performing system checks...
3
4System check identified no issues (0 silenced).
5September 28, 2014 - 20:16:47
6Django version 1.7, using settings 'django_tutorial_blog_ng.settings'
7Starting development server at http://127.0.0.1:8000/
8Quit the server with CONTROL-C.
9Unhandled exception in thread started by <function wrapper at 0x1024a5ed8>
10Traceback (most recent call last):
11 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/utils/autoreload.py", line 222, in wrapper
12 fn(*args, **kwargs)
13 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/management/commands/runserver.py", line 132, in inner_run
14 handler = self.get_handler(*args, **options)
15 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/contrib/staticfiles/management/commands/runserver.py", line 25, in get_handler
16 handler = super(Command, self).get_handler(*args, **options)
17 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/management/commands/runserver.py", line 48, in get_handler
18 return get_internal_wsgi_application()
19 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/servers/basehttp.py", line 66, in get_internal_wsgi_application
20 sys.exc_info()[2])
21 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/servers/basehttp.py", line 56, in get_internal_wsgi_application
22 return import_string(app_path)
23 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/utils/module_loading.py", line 26, in import_string
24 module = import_module(module_path)
25 File "/usr/local/Cellar/python/2.7.8_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/importlib/__init__.py", line 37, in import_module
26 __import__(name)
27 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/django_tutorial_blog_ng/wsgi.py", line 14, in <module>
28 from dj_static import Cling
29 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/dj_static.py", line 7, in <module>
30 from django.core.handlers.base import get_path_info
31django.core.exceptions.ImproperlyConfigured: WSGI application 'django_tutorial_blog_ng.wsgi.application' could not be loaded; Error importing module: 'cannot import name get_path_info'

Fortunately, the error above is easy to fix by upgrading dj_static:

1$ pip install dj_static --upgrade
2$ pip freeze > requirements.txt

That resolves the error in serving static files, but not the error with the admin. If you run the dev server, you'll be able to see that the admin actually works fine. The problem is caused by the test client not following redirects in the admin. We can easily run just the admin tests with the following command:

1$ python manage.py test blogengine.tests.AdminTest
2Creating test database for alias 'default'...
3.FF.F.FFFFFF
4======================================================================
5FAIL: test_create_post (blogengine.tests.AdminTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 385, in test_create_post
9 self.assertTrue('added successfully' in response.content)
10AssertionError: False is not true
11
12======================================================================
13FAIL: test_create_post_without_tag (blogengine.tests.AdminTest)
14----------------------------------------------------------------------
15Traceback (most recent call last):
16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 417, in test_create_post_without_tag
17 self.assertTrue('added successfully' in response.content)
18AssertionError: False is not true
19
20======================================================================
21FAIL: test_delete_category (blogengine.tests.AdminTest)
22----------------------------------------------------------------------
23Traceback (most recent call last):
24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 278, in test_delete_category
25 self.assertEquals(response.status_code, 200)
26AssertionError: 404 != 200
27
28======================================================================
29FAIL: test_delete_tag (blogengine.tests.AdminTest)
30----------------------------------------------------------------------
31Traceback (most recent call last):
32 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 346, in test_delete_tag
33 self.assertEquals(response.status_code, 200)
34AssertionError: 404 != 200
35
36======================================================================
37FAIL: test_edit_category (blogengine.tests.AdminTest)
38----------------------------------------------------------------------
39Traceback (most recent call last):
40 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 255, in test_edit_category
41 self.assertEquals(response.status_code, 200)
42AssertionError: 404 != 200
43
44======================================================================
45FAIL: test_edit_post (blogengine.tests.AdminTest)
46----------------------------------------------------------------------
47Traceback (most recent call last):
48 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 447, in test_edit_post
49 self.assertEquals(response.status_code, 200)
50AssertionError: 404 != 200
51
52======================================================================
53FAIL: test_edit_tag (blogengine.tests.AdminTest)
54----------------------------------------------------------------------
55Traceback (most recent call last):
56 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 323, in test_edit_tag
57 self.assertEquals(response.status_code, 200)
58AssertionError: 404 != 200
59
60======================================================================
61FAIL: test_login (blogengine.tests.AdminTest)
62----------------------------------------------------------------------
63Traceback (most recent call last):
64 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 183, in test_login
65 self.assertEquals(response.status_code, 200)
66AssertionError: 302 != 200
67
68======================================================================
69FAIL: test_logout (blogengine.tests.AdminTest)
70----------------------------------------------------------------------
71Traceback (most recent call last):
72 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 214, in test_logout
73 self.assertEquals(response.status_code, 200)
74AssertionError: 302 != 200
75
76----------------------------------------------------------------------
77Ran 12 tests in 3.283s
78
79FAILED (failures=9)
80Destroying test database for alias 'default'...

Let's commit our changes so far first:

1$ git add django_tutorial_blog_ng/ requirements.txt blogengine/
2$ git commit -m 'Upgraded to Django 1.7'

Now let's fix our tests. Here's the amended version of the AdminTest class:

1class AdminTest(BaseAcceptanceTest):
2 fixtures = ['users.json']
3
4 def test_login(self):
5 # Get login page
6 response = self.client.get('/admin/', follow=True)
7
8 # Check response code
9 self.assertEquals(response.status_code, 200)
10
11 # Check 'Log in' in response
12 self.assertTrue('Log in' in response.content)
13
14 # Log the user in
15 self.client.login(username='bobsmith', password="password")
16
17 # Check response code
18 response = self.client.get('/admin/')
19 self.assertEquals(response.status_code, 200)
20
21 # Check 'Log out' in response
22 self.assertTrue('Log out' in response.content)
23
24 def test_logout(self):
25 # Log in
26 self.client.login(username='bobsmith', password="password")
27
28 # Check response code
29 response = self.client.get('/admin/')
30 self.assertEquals(response.status_code, 200)
31
32 # Check 'Log out' in response
33 self.assertTrue('Log out' in response.content)
34
35 # Log out
36 self.client.logout()
37
38 # Check response code
39 response = self.client.get('/admin/', follow=True)
40 self.assertEquals(response.status_code, 200)
41
42 # Check 'Log in' in response
43 self.assertTrue('Log in' in response.content)
44
45 def test_create_category(self):
46 # Log in
47 self.client.login(username='bobsmith', password="password")
48
49 # Check response code
50 response = self.client.get('/admin/blogengine/category/add/')
51 self.assertEquals(response.status_code, 200)
52
53 # Create the new category
54 response = self.client.post('/admin/blogengine/category/add/', {
55 'name': 'python',
56 'description': 'The Python programming language'
57 },
58 follow=True
59 )
60 self.assertEquals(response.status_code, 200)
61
62 # Check added successfully
63 self.assertTrue('added successfully' in response.content)
64
65 # Check new category now in database
66 all_categories = Category.objects.all()
67 self.assertEquals(len(all_categories), 1)
68
69 def test_edit_category(self):
70 # Create the category
71 category = CategoryFactory()
72
73 # Log in
74 self.client.login(username='bobsmith', password="password")
75
76 # Edit the category
77 response = self.client.post('/admin/blogengine/category/' + str(category.pk) + '/', {
78 'name': 'perl',
79 'description': 'The Perl programming language'
80 }, follow=True)
81 self.assertEquals(response.status_code, 200)
82
83 # Check changed successfully
84 self.assertTrue('changed successfully' in response.content)
85
86 # Check category amended
87 all_categories = Category.objects.all()
88 self.assertEquals(len(all_categories), 1)
89 only_category = all_categories[0]
90 self.assertEquals(only_category.name, 'perl')
91 self.assertEquals(only_category.description, 'The Perl programming language')
92
93 def test_delete_category(self):
94 # Create the category
95 category = CategoryFactory()
96
97 # Log in
98 self.client.login(username='bobsmith', password="password")
99
100 # Delete the category
101 response = self.client.post('/admin/blogengine/category/' + str(category.pk) + '/delete/', {
102 'post': 'yes'
103 }, follow=True)
104 self.assertEquals(response.status_code, 200)
105
106 # Check deleted successfully
107 self.assertTrue('deleted successfully' in response.content)
108
109 # Check category deleted
110 all_categories = Category.objects.all()
111 self.assertEquals(len(all_categories), 0)
112
113 def test_create_tag(self):
114 # Log in
115 self.client.login(username='bobsmith', password="password")
116
117 # Check response code
118 response = self.client.get('/admin/blogengine/tag/add/')
119 self.assertEquals(response.status_code, 200)
120
121 # Create the new tag
122 response = self.client.post('/admin/blogengine/tag/add/', {
123 'name': 'python',
124 'description': 'The Python programming language'
125 },
126 follow=True
127 )
128 self.assertEquals(response.status_code, 200)
129
130 # Check added successfully
131 self.assertTrue('added successfully' in response.content)
132
133 # Check new tag now in database
134 all_tags = Tag.objects.all()
135 self.assertEquals(len(all_tags), 1)
136
137 def test_edit_tag(self):
138 # Create the tag
139 tag = TagFactory()
140
141 # Log in
142 self.client.login(username='bobsmith', password="password")
143
144 # Edit the tag
145 response = self.client.post('/admin/blogengine/tag/' + str(tag.pk) + '/', {
146 'name': 'perl',
147 'description': 'The Perl programming language'
148 }, follow=True)
149 self.assertEquals(response.status_code, 200)
150
151 # Check changed successfully
152 self.assertTrue('changed successfully' in response.content)
153
154 # Check tag amended
155 all_tags = Tag.objects.all()
156 self.assertEquals(len(all_tags), 1)
157 only_tag = all_tags[0]
158 self.assertEquals(only_tag.name, 'perl')
159 self.assertEquals(only_tag.description, 'The Perl programming language')
160
161 def test_delete_tag(self):
162 # Create the tag
163 tag = TagFactory()
164
165 # Log in
166 self.client.login(username='bobsmith', password="password")
167
168 # Delete the tag
169 response = self.client.post('/admin/blogengine/tag/' + str(tag.pk) + '/delete/', {
170 'post': 'yes'
171 }, follow=True)
172 self.assertEquals(response.status_code, 200)
173
174 # Check deleted successfully
175 self.assertTrue('deleted successfully' in response.content)
176
177 # Check tag deleted
178 all_tags = Tag.objects.all()
179 self.assertEquals(len(all_tags), 0)
180
181 def test_create_post(self):
182 # Create the category
183 category = CategoryFactory()
184
185 # Create the tag
186 tag = TagFactory()
187
188 # Log in
189 self.client.login(username='bobsmith', password="password")
190
191 # Check response code
192 response = self.client.get('/admin/blogengine/post/add/')
193 self.assertEquals(response.status_code, 200)
194
195 # Create the new post
196 response = self.client.post('/admin/blogengine/post/add/', {
197 'title': 'My first post',
198 'text': 'This is my first post',
199 'pub_date_0': '2013-12-28',
200 'pub_date_1': '22:00:04',
201 'slug': 'my-first-post',
202 'site': '1',
203 'category': str(category.pk),
204 'tags': str(tag.pk)
205 },
206 follow=True
207 )
208 self.assertEquals(response.status_code, 200)
209
210 # Check added successfully
211 self.assertTrue('added successfully' in response.content)
212
213 # Check new post now in database
214 all_posts = Post.objects.all()
215 self.assertEquals(len(all_posts), 1)
216
217 def test_create_post_without_tag(self):
218 # Create the category
219 category = CategoryFactory()
220
221 # Log in
222 self.client.login(username='bobsmith', password="password")
223
224 # Check response code
225 response = self.client.get('/admin/blogengine/post/add/')
226 self.assertEquals(response.status_code, 200)
227
228 # Create the new post
229 response = self.client.post('/admin/blogengine/post/add/', {
230 'title': 'My first post',
231 'text': 'This is my first post',
232 'pub_date_0': '2013-12-28',
233 'pub_date_1': '22:00:04',
234 'slug': 'my-first-post',
235 'site': '1',
236 'category': str(category.pk)
237 },
238 follow=True
239 )
240 self.assertEquals(response.status_code, 200)
241
242 # Check added successfully
243 self.assertTrue('added successfully' in response.content)
244
245 # Check new post now in database
246 all_posts = Post.objects.all()
247 self.assertEquals(len(all_posts), 1)
248
249 def test_edit_post(self):
250 # Create the post
251 post = PostFactory()
252
253 # Create the category
254 category = CategoryFactory()
255
256 # Create the tag
257 tag = TagFactory()
258 post.tags.add(tag)
259
260 # Log in
261 self.client.login(username='bobsmith', password="password")
262
263 # Edit the post
264 response = self.client.post('/admin/blogengine/post/' + str(post.pk) + '/', {
265 'title': 'My second post',
266 'text': 'This is my second blog post',
267 'pub_date_0': '2013-12-28',
268 'pub_date_1': '22:00:04',
269 'slug': 'my-second-post',
270 'site': '1',
271 'category': str(category.pk),
272 'tags': str(tag.pk)
273 },
274 follow=True
275 )
276 self.assertEquals(response.status_code, 200)
277
278 # Check changed successfully
279 self.assertTrue('changed successfully' in response.content)
280
281 # Check post amended
282 all_posts = Post.objects.all()
283 self.assertEquals(len(all_posts), 1)
284 only_post = all_posts[0]
285 self.assertEquals(only_post.title, 'My second post')
286 self.assertEquals(only_post.text, 'This is my second blog post')
287
288 def test_delete_post(self):
289 # Create the post
290 post = PostFactory()
291
292 # Create the tag
293 tag = TagFactory()
294 post.tags.add(tag)
295
296 # Check new post saved
297 all_posts = Post.objects.all()
298 self.assertEquals(len(all_posts), 1)
299
300 # Log in
301 self.client.login(username='bobsmith', password="password")
302
303 # Delete the post
304 response = self.client.post('/admin/blogengine/post/' + str(post.pk) + '/delete/', {
305 'post': 'yes'
306 }, follow=True)
307 self.assertEquals(response.status_code, 200)
308
309 # Check deleted successfully
310 self.assertTrue('deleted successfully' in response.content)
311
312 # Check post deleted
313 all_posts = Post.objects.all()
314 self.assertEquals(len(all_posts), 0)

There are two main issues here. The first is that when we try to edit or delete an existing item, or refer to it when creating something else, we can no longer rely on the number representing the primary key being set to 1. So we need to specifically obtain this, rather than hard-coding it to 1. Therefore, whenever we pass through a number to represent an item (with the exception of the site, but including tags, categories and posts), we need to instead fetch its primary key and return it. So, above where we try to delete a post, we replace 1 with str(post.pk). This will solve a lot of the problems. As there's a lot of them, I won't go through each one, but you can see the entire class above for reference, and if you've followed along so far, you shouldn't have any problems.

The other issue we need to fix is the login and logout tests. We simply add follow=True to these to ensure that the test client follows the redirects.

Let's run our tests to make sure they pass:

1$ python manage.py jenkins
2Creating test database for alias 'default'...
3.............................
4----------------------------------------------------------------------
5Ran 29 tests in 8.210s
6
7OK
8Destroying test database for alias 'default'...

With that done, you can commit your changes:

1$ git add blogengine/tests.py
2$ git commit -m 'Fixed broken tests'

Don't forget to deploy your changes:

$ fab deploy

Our blog has now been happily migrated over to Django 1.7!