Django Blog Tutorial - the Next Generation - Part 9
Published by Matthew Daly 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 syncdb2$ python manage.py migrate
Then, upgrade your Django version and uninstall South:
1$ pip install Django --upgrade2$ pip uninstall South3$ 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 makemigrations2Migrations for 'blogengine':3 0001_initial.py:4 - Create model Category5 - Create model Post6 - Create model Tag7 - Add field tags to post
Then we run the migrations:
1$ python manage.py migrate2Operations to perform:3 Synchronize unmigrated apps: sitemaps, django_jenkins, debug_toolbar4 Apply all migrations: sessions, admin, sites, flatpages, contenttypes, auth, blogengine5Synchronizing apps without migrations:6 Creating tables...7 Installing custom SQL...8 Installing indexes...9Running migrations:10 Applying contenttypes.0001_initial... FAKED11 Applying auth.0001_initial... FAKED12 Applying admin.0001_initial... FAKED13 Applying sites.0001_initial... FAKED14 Applying blogengine.0001_initial... FAKED15 Applying flatpages.0001_initial... FAKED16 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 jenkins2Creating 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_post9 self.assertTrue('added successfully' in response.content)10AssertionError: False is not true1112======================================================================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_tag17 self.assertTrue('added successfully' in response.content)18AssertionError: False is not true1920======================================================================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_category25 self.assertEquals(response.status_code, 200)26AssertionError: 404 != 2002728======================================================================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_tag33 self.assertEquals(response.status_code, 200)34AssertionError: 404 != 2003536======================================================================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_category41 self.assertEquals(response.status_code, 200)42AssertionError: 404 != 2004344======================================================================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_post49 self.assertEquals(response.status_code, 200)50AssertionError: 404 != 2005152======================================================================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_tag57 self.assertEquals(response.status_code, 200)58AssertionError: 404 != 2005960======================================================================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_login65 self.assertEquals(response.status_code, 200)66AssertionError: 302 != 2006768======================================================================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_logout73 self.assertEquals(response.status_code, 200)74AssertionError: 302 != 2007576----------------------------------------------------------------------77Ran 29 tests in 7.383s7879FAILED (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 runserver2Performing system checks...34System check identified no issues (0 silenced).5September 28, 2014 - 20:16:476Django 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 wrapper12 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_run14 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_handler16 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_handler18 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_application20 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_application22 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_string24 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_module26 __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 Cling29 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_info31django.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 --upgrade2$ 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.AdminTest2Creating test database for alias 'default'...3.FF.F.FFFFFF4======================================================================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_post9 self.assertTrue('added successfully' in response.content)10AssertionError: False is not true1112======================================================================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_tag17 self.assertTrue('added successfully' in response.content)18AssertionError: False is not true1920======================================================================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_category25 self.assertEquals(response.status_code, 200)26AssertionError: 404 != 2002728======================================================================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_tag33 self.assertEquals(response.status_code, 200)34AssertionError: 404 != 2003536======================================================================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_category41 self.assertEquals(response.status_code, 200)42AssertionError: 404 != 2004344======================================================================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_post49 self.assertEquals(response.status_code, 200)50AssertionError: 404 != 2005152======================================================================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_tag57 self.assertEquals(response.status_code, 200)58AssertionError: 404 != 2005960======================================================================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_login65 self.assertEquals(response.status_code, 200)66AssertionError: 302 != 2006768======================================================================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_logout73 self.assertEquals(response.status_code, 200)74AssertionError: 302 != 2007576----------------------------------------------------------------------77Ran 12 tests in 3.283s7879FAILED (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']34 def test_login(self):5 # Get login page6 response = self.client.get('/admin/', follow=True)78 # Check response code9 self.assertEquals(response.status_code, 200)1011 # Check 'Log in' in response12 self.assertTrue('Log in' in response.content)1314 # Log the user in15 self.client.login(username='bobsmith', password="password")1617 # Check response code18 response = self.client.get('/admin/')19 self.assertEquals(response.status_code, 200)2021 # Check 'Log out' in response22 self.assertTrue('Log out' in response.content)2324 def test_logout(self):25 # Log in26 self.client.login(username='bobsmith', password="password")2728 # Check response code29 response = self.client.get('/admin/')30 self.assertEquals(response.status_code, 200)3132 # Check 'Log out' in response33 self.assertTrue('Log out' in response.content)3435 # Log out36 self.client.logout()3738 # Check response code39 response = self.client.get('/admin/', follow=True)40 self.assertEquals(response.status_code, 200)4142 # Check 'Log in' in response43 self.assertTrue('Log in' in response.content)4445 def test_create_category(self):46 # Log in47 self.client.login(username='bobsmith', password="password")4849 # Check response code50 response = self.client.get('/admin/blogengine/category/add/')51 self.assertEquals(response.status_code, 200)5253 # Create the new category54 response = self.client.post('/admin/blogengine/category/add/', {55 'name': 'python',56 'description': 'The Python programming language'57 },58 follow=True59 )60 self.assertEquals(response.status_code, 200)6162 # Check added successfully63 self.assertTrue('added successfully' in response.content)6465 # Check new category now in database66 all_categories = Category.objects.all()67 self.assertEquals(len(all_categories), 1)6869 def test_edit_category(self):70 # Create the category71 category = CategoryFactory()7273 # Log in74 self.client.login(username='bobsmith', password="password")7576 # Edit the category77 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)8283 # Check changed successfully84 self.assertTrue('changed successfully' in response.content)8586 # Check category amended87 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')9293 def test_delete_category(self):94 # Create the category95 category = CategoryFactory()9697 # Log in98 self.client.login(username='bobsmith', password="password")99100 # Delete the category101 response = self.client.post('/admin/blogengine/category/' + str(category.pk) + '/delete/', {102 'post': 'yes'103 }, follow=True)104 self.assertEquals(response.status_code, 200)105106 # Check deleted successfully107 self.assertTrue('deleted successfully' in response.content)108109 # Check category deleted110 all_categories = Category.objects.all()111 self.assertEquals(len(all_categories), 0)112113 def test_create_tag(self):114 # Log in115 self.client.login(username='bobsmith', password="password")116117 # Check response code118 response = self.client.get('/admin/blogengine/tag/add/')119 self.assertEquals(response.status_code, 200)120121 # Create the new tag122 response = self.client.post('/admin/blogengine/tag/add/', {123 'name': 'python',124 'description': 'The Python programming language'125 },126 follow=True127 )128 self.assertEquals(response.status_code, 200)129130 # Check added successfully131 self.assertTrue('added successfully' in response.content)132133 # Check new tag now in database134 all_tags = Tag.objects.all()135 self.assertEquals(len(all_tags), 1)136137 def test_edit_tag(self):138 # Create the tag139 tag = TagFactory()140141 # Log in142 self.client.login(username='bobsmith', password="password")143144 # Edit the tag145 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)150151 # Check changed successfully152 self.assertTrue('changed successfully' in response.content)153154 # Check tag amended155 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')160161 def test_delete_tag(self):162 # Create the tag163 tag = TagFactory()164165 # Log in166 self.client.login(username='bobsmith', password="password")167168 # Delete the tag169 response = self.client.post('/admin/blogengine/tag/' + str(tag.pk) + '/delete/', {170 'post': 'yes'171 }, follow=True)172 self.assertEquals(response.status_code, 200)173174 # Check deleted successfully175 self.assertTrue('deleted successfully' in response.content)176177 # Check tag deleted178 all_tags = Tag.objects.all()179 self.assertEquals(len(all_tags), 0)180181 def test_create_post(self):182 # Create the category183 category = CategoryFactory()184185 # Create the tag186 tag = TagFactory()187188 # Log in189 self.client.login(username='bobsmith', password="password")190191 # Check response code192 response = self.client.get('/admin/blogengine/post/add/')193 self.assertEquals(response.status_code, 200)194195 # Create the new post196 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=True207 )208 self.assertEquals(response.status_code, 200)209210 # Check added successfully211 self.assertTrue('added successfully' in response.content)212213 # Check new post now in database214 all_posts = Post.objects.all()215 self.assertEquals(len(all_posts), 1)216217 def test_create_post_without_tag(self):218 # Create the category219 category = CategoryFactory()220221 # Log in222 self.client.login(username='bobsmith', password="password")223224 # Check response code225 response = self.client.get('/admin/blogengine/post/add/')226 self.assertEquals(response.status_code, 200)227228 # Create the new post229 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=True239 )240 self.assertEquals(response.status_code, 200)241242 # Check added successfully243 self.assertTrue('added successfully' in response.content)244245 # Check new post now in database246 all_posts = Post.objects.all()247 self.assertEquals(len(all_posts), 1)248249 def test_edit_post(self):250 # Create the post251 post = PostFactory()252253 # Create the category254 category = CategoryFactory()255256 # Create the tag257 tag = TagFactory()258 post.tags.add(tag)259260 # Log in261 self.client.login(username='bobsmith', password="password")262263 # Edit the post264 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=True275 )276 self.assertEquals(response.status_code, 200)277278 # Check changed successfully279 self.assertTrue('changed successfully' in response.content)280281 # Check post amended282 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')287288 def test_delete_post(self):289 # Create the post290 post = PostFactory()291292 # Create the tag293 tag = TagFactory()294 post.tags.add(tag)295296 # Check new post saved297 all_posts = Post.objects.all()298 self.assertEquals(len(all_posts), 1)299300 # Log in301 self.client.login(username='bobsmith', password="password")302303 # Delete the post304 response = self.client.post('/admin/blogengine/post/' + str(post.pk) + '/delete/', {305 'post': 'yes'306 }, follow=True)307 self.assertEquals(response.status_code, 200)308309 # Check deleted successfully310 self.assertTrue('deleted successfully' in response.content)311312 # Check post deleted313 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 jenkins2Creating test database for alias 'default'...3.............................4----------------------------------------------------------------------5Ran 29 tests in 8.210s67OK8Destroying test database for alias 'default'...
With that done, you can commit your changes:
1$ git add blogengine/tests.py2$ 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!