Django Blog Tutorial - the Next Generation - Part 3
Published by Matthew Daly at 3rd January 2014 12:57 pm
Hello again! In this instalment, we're going to do the following:
- Add support for flat pages
- Add support for multiple authors
- Add a third-party comment system
Flat pages
Django ships with a number of useful apps - we've already used the admin interface. The flat pages app is another very handy app that comes with Django, and we'll use it to allow the blog author to create a handful of flat pages.
First of all, you'll need to install the flatpages app. Edit the INSTALLED_APPS
setting as follows:
1INSTALLED_APPS = (2 'django.contrib.admin',3 'django.contrib.auth',4 'django.contrib.contenttypes',5 'django.contrib.sessions',6 'django.contrib.messages',7 'django.contrib.staticfiles',8 'south',9 'blogengine',10 'django.contrib.sites',11 'django.contrib.flatpages',12)
Note that we needed to enable the sites
framework as well. You'll also need to set the SITE_ID
setting:
SITE_ID = 1
With that done, run python manage.py syncdb
to create the required database tables. Now, let's use the sqlall
command to take a look at the database structure generated for the flat pages:
1BEGIN;2CREATE TABLE "django_flatpage_sites" (3 "id" integer NOT NULL PRIMARY KEY,4 "flatpage_id" integer NOT NULL,5 "site_id" integer NOT NULL REFERENCES "django_site" ("id"),6 UNIQUE ("flatpage_id", "site_id")7)8;9CREATE TABLE "django_flatpage" (10 "id" integer NOT NULL PRIMARY KEY,11 "url" varchar(100) NOT NULL,12 "title" varchar(200) NOT NULL,13 "content" text NOT NULL,14 "enable_comments" bool NOT NULL,15 "template_name" varchar(70) NOT NULL,16 "registration_required" bool NOT NULL17)18;19CREATE INDEX "django_flatpage_sites_872c4601" ON "django_flatpage_sites" ("flatpage_id");20CREATE INDEX "django_flatpage_sites_99732b5c" ON "django_flatpage_sites" ("site_id");21CREATE INDEX "django_flatpage_c379dc61" ON "django_flatpage" ("url");2223COMMIT;
As mentioned previously, all models in Django have an id
attribute by default. Each flat page also has a URL, title, and content.
Also note the separate django_flatpage_sites
table, which maps sites to flat pages. Django can run multiple sites from the same web app, and so flat pages must be allocated to a specific site. This relationship is a many-to-many relationship, so one flat page can appear on more than one site.
The other fields are hidden by default in the admin and can be ignored. Let's have a go with Django's handy shell to explore the flatpage. Run python manage.py shell
and you'll be able to interact with your Django application interactively:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py shell2Python 2.7.6 (default, Nov 23 2013, 13:53:45)3[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin4Type "help", "copyright", "credits" or "license" for more information.5(InteractiveConsole)6>>> from django.contrib.flatpages.models import *7>>> FlatPage8<class 'django.contrib.flatpages.models.FlatPage'>9>>> from django.contrib.sites.models import Site10>>> Site.objects.all()11[<Site: example.com>]
As you can see, flatpages
is a Django app similar to the blogengine
one, with its own models, as is sites
. You can see that the FlatPage
class is a model. We can create an instance of it and save it interactively:
1>>> f = FlatPage()2>>> f.url = '/about/'3>>> f.title = 'About me'4>>> f.content = 'All about me'5>>> f.save()6>>> f.sites.add(Site.objects.all()[0])7>>> f.save()
Note that because the relationship between the site and the flat page is a many-to-many relationship, we need to save it first, then use the add
method to add the site to the list of sites.
We can retrieve it:
1>>> FlatPage.objects.all()2[<FlatPage: /about/ -- About me>]3>>> FlatPage.objects.all()[0]4<FlatPage: /about/ -- About me>5>>> FlatPage.objects.all()[0].title6u'About me'
This command is often handy for debugging problems with your models interactively. If you now run the server and visit the admin, you should notice that the Flatpages app is now visible there, and the 'About me' flat page is now shown in there.
Let's also take a look at the SQL required for the Site
model. Run python manage.py sqlall sites
:
1BEGIN;2CREATE TABLE "django_site" (3 "id" integer NOT NULL PRIMARY KEY,4 "domain" varchar(100) NOT NULL,5 "name" varchar(50) NOT NULL6)7;89COMMIT;
Again, very simple - just a domain and a name.
So, now that we have a good idea of how the flat page system works, we can write a test for it. We don't need to write unit tests for the model because Django already does that, but we do need to write an acceptance test to ensure we can create flat pages and they will be where we expect them to be. Add the following to the top of the test file:
1from django.contrib.flatpages.models import FlatPage2from django.contrib.sites.models import Site
Now, before we write this test, there's some duplication to resolve. We have two tests that subclass LiveServerTestCase
, and both have the same method, setUp
. We can save ourselves some hassle by creating a new class containing this method and having both these tests inherit from it. We'll do that now because the flat page test can also be based on it. Create the following class just after PostTest
:
1class BaseAcceptanceTest(LiveServerTestCase):2 def setUp(self):3 self.client = Client()
Then remove the setUp method from each of the two tests based on LiveServerTestCase
, and change their parent class to BaseAcceptanceTest
:
1class AdminTest(BaseAcceptanceTest):23class PostViewTest(BaseAcceptanceTest):
With that done, run the tests and they should pass. Commit your changes:
1$ git add blogengine/tests.py django_tutorial_blog_ng/settings.py2$ git commit -m 'Added flatpages to installed apps'
Now we can get started in earnest on our test for the flat pages:
1class FlatPageViewTest(BaseAcceptanceTest):2 def test_create_flat_page(self):3 # Create flat page4 page = FlatPage()5 page.url = '/about/'6 page.title = 'About me'7 page.content = 'All about me'8 page.save()910 # Add the site11 page.sites.add(Site.objects.all()[0])12 page.save()1314 # Check new page saved15 all_pages = FlatPage.objects.all()16 self.assertEquals(len(all_pages), 1)17 only_page = all_pages[0]18 self.assertEquals(only_page, page)1920 # Check data correct21 self.assertEquals(only_page.url, '/about/')22 self.assertEquals(only_page.title, 'About me')23 self.assertEquals(only_page.content, 'All about me')2425 # Get URL26 page_url = only_page.get_absolute_url()2728 # Get the page29 response = self.client.get(page_url)30 self.assertEquals(response.status_code, 200)3132 # Check title and content in response33 self.assertTrue('About me' in response.content)34 self.assertTrue('All about me' in response.content)
Let's run our tests:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test2Creating test database for alias 'default'...3......F..4======================================================================5FAIL: test_create_flat_page (blogengine.tests.FlatPageViewTest)6----------------------------------------------------------------------7Traceback (most recent call last):8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 272, in test_create_flat_page9 self.assertEquals(response.status_code, 200)10AssertionError: 404 != 2001112----------------------------------------------------------------------13Ran 9 tests in 2.760s1415FAILED (failures=1)
We can see why it's failed - in our flat page test, the status code is 404, indicating the page was not found. This just means we haven't put flat page support into our URLconf. So let's fix that:
1from django.conf.urls import patterns, include, url23from django.contrib import admin4admin.autodiscover()56urlpatterns = patterns('',7 # Examples:8 # url(r'^$', 'django_tutorial_blog_ng.views.home', name='home'),9 # url(r'^blog/', include('blog.urls')),1011 url(r'^admin/', include(admin.site.urls)),1213 # Blog URLs14 url(r'', include('blogengine.urls')),1516 # Flat pages17 url(r'', include('django.contrib.flatpages.urls')),18)
Let's run our tests again:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test2Creating test database for alias 'default'...3......E..4======================================================================5ERROR: test_create_flat_page (blogengine.tests.FlatPageViewTest)6----------------------------------------------------------------------7Traceback (most recent call last):8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 276, in test_create_flat_page9 response = self.client.get(page_url)10 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/test/client.py", line 473, in get11 response = super(Client, self).get(path, data=data, **extra)12 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/test/client.py", line 280, in get13 return self.request(**r)14 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/test/client.py", line 444, in request15 six.reraise(*exc_info)16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/handlers/base.py", line 114, in get_response17 response = wrapped_callback(request, *callback_args, **callback_kwargs)18 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/contrib/flatpages/views.py", line 45, in flatpage19 return render_flatpage(request, f)20 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/utils/decorators.py", line 99, in _wrapped_view21 response = view_func(request, *args, **kwargs)22 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/contrib/flatpages/views.py", line 60, in render_flatpage23 t = loader.get_template(DEFAULT_TEMPLATE)24 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/template/loader.py", line 138, in get_template25 template, origin = find_template(template_name)26 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/template/loader.py", line 131, in find_template27 raise TemplateDoesNotExist(name)28TemplateDoesNotExist: flatpages/default.html2930----------------------------------------------------------------------31Ran 9 tests in 3.557s3233FAILED (errors=1)34Destroying test database for alias 'default'...
Our test still fails, but we can easily see why - the template flatpages/default.html
doesn't exist. So we create it:
1{% extends "blogengine/includes/base.html" %}23 {% load custom_markdown %}45 {% block content %}6 <div class="post">7 <h1>{{ flatpage.title }}</h1>8 {{ flatpage.content|custom_markdown }}9 </div>1011 {% endblock %}
This template is based on the blog post one, and just changes a handful of variable names. Note that it can still inherit from the blogengine base template, and in this case we're using that for the sake of consistency.
If you run your tests, you should now see that they pass, so we'll commit our changes:
1$ git add templates/ django_tutorial_blog_ng/ blogengine/2$ git commit -m 'Implemented flat page support'
Multiple authors
Next we'll add support for multiple authors. Now, Django already has a User model, and we'll leverage that to represent the authors. But first we'll write our test:
1from django.test import TestCase, LiveServerTestCase, Client2from django.utils import timezone3from blogengine.models import Post4from django.contrib.flatpages.models import FlatPage5from django.contrib.sites.models import Site6from django.contrib.auth.models import User7import markdown89# Create your tests here.10class PostTest(TestCase):11 def test_create_post(self):12 # Create the author13 author = User.objects.create_user('testuser', 'user@example.com', 'password')14 author.save()1516 # Create the post17 post = Post()1819 # Set the attributes20 post.title = 'My first post'21 post.text = 'This is my first blog post'22 post.slug = 'my-first-post'23 post.pub_date = timezone.now()24 post.author = author2526 # Save it27 post.save()2829 # Check we can find it30 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 # Check attributes36 self.assertEquals(only_post.title, 'My first post')37 self.assertEquals(only_post.text, 'This is my first blog post')38 self.assertEquals(only_post.slug, 'my-first-post')39 self.assertEquals(only_post.pub_date.day, post.pub_date.day)40 self.assertEquals(only_post.pub_date.month, post.pub_date.month)41 self.assertEquals(only_post.pub_date.year, post.pub_date.year)42 self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)43 self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)44 self.assertEquals(only_post.pub_date.second, post.pub_date.second)45 self.assertEquals(only_post.author.username, 'testuser')46 self.assertEquals(only_post.author.email, 'user@example.com')4748class BaseAcceptanceTest(LiveServerTestCase):49 def setUp(self):50 self.client = Client()515253class AdminTest(BaseAcceptanceTest):54 fixtures = ['users.json']5556 def test_login(self):57 # Get login page58 response = self.client.get('/admin/')5960 # Check response code61 self.assertEquals(response.status_code, 200)6263 # Check 'Log in' in response64 self.assertTrue('Log in' in response.content)6566 # Log the user in67 self.client.login(username='bobsmith', password="password")6869 # Check response code70 response = self.client.get('/admin/')71 self.assertEquals(response.status_code, 200)7273 # Check 'Log out' in response74 self.assertTrue('Log out' in response.content)7576 def test_logout(self):77 # Log in78 self.client.login(username='bobsmith', password="password")7980 # Check response code81 response = self.client.get('/admin/')82 self.assertEquals(response.status_code, 200)8384 # Check 'Log out' in response85 self.assertTrue('Log out' in response.content)8687 # Log out88 self.client.logout()8990 # Check response code91 response = self.client.get('/admin/')92 self.assertEquals(response.status_code, 200)9394 # Check 'Log in' in response95 self.assertTrue('Log in' in response.content)9697 def test_create_post(self):98 # Log in99 self.client.login(username='bobsmith', password="password")100101 # Check response code102 response = self.client.get('/admin/blogengine/post/add/')103 self.assertEquals(response.status_code, 200)104105 # Create the new post106 response = self.client.post('/admin/blogengine/post/add/', {107 'title': 'My first post',108 'text': 'This is my first post',109 'pub_date_0': '2013-12-28',110 'pub_date_1': '22:00:04',111 'slug': 'my-first-post'112 },113 follow=True114 )115 self.assertEquals(response.status_code, 200)116117 # Check added successfully118 self.assertTrue('added successfully' in response.content)119120 # Check new post now in database121 all_posts = Post.objects.all()122 self.assertEquals(len(all_posts), 1)123124 def test_edit_post(self):125 # Create the author126 author = User.objects.create_user('testuser', 'user@example.com', 'password')127 author.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.author = author136 post.save()137138 # Log in139 self.client.login(username='bobsmith', password="password")140141 # Edit the post142 response = self.client.post('/admin/blogengine/post/1/', {143 'title': 'My second post',144 'text': 'This is my second blog post',145 'pub_date_0': '2013-12-28',146 'pub_date_1': '22:00:04',147 'slug': 'my-second-post'148 },149 follow=True150 )151 self.assertEquals(response.status_code, 200)152153 # Check changed successfully154 self.assertTrue('changed successfully' in response.content)155156 # Check post amended157 all_posts = Post.objects.all()158 self.assertEquals(len(all_posts), 1)159 only_post = all_posts[0]160 self.assertEquals(only_post.title, 'My second post')161 self.assertEquals(only_post.text, 'This is my second blog post')162163 def test_delete_post(self):164 # Create the author165 author = User.objects.create_user('testuser', 'user@example.com', 'password')166 author.save()167168 # Create the post169 post = Post()170 post.title = 'My first post'171 post.text = 'This is my first blog post'172 post.slug = 'my-first-post'173 post.pub_date = timezone.now()174 post.author = author175 post.save()176177 # Check new post saved178 all_posts = Post.objects.all()179 self.assertEquals(len(all_posts), 1)180181 # Log in182 self.client.login(username='bobsmith', password="password")183184 # Delete the post185 response = self.client.post('/admin/blogengine/post/1/delete/', {186 'post': 'yes'187 }, follow=True)188 self.assertEquals(response.status_code, 200)189190 # Check deleted successfully191 self.assertTrue('deleted successfully' in response.content)192193 # Check post amended194 all_posts = Post.objects.all()195 self.assertEquals(len(all_posts), 0)196197class PostViewTest(BaseAcceptanceTest):198 def test_index(self):199 # Create the author200 author = User.objects.create_user('testuser', 'user@example.com', 'password')201 author.save()202203 # Create the post204 post = Post()205 post.title = 'My first post'206 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'207 post.slug = 'my-first-post'208 post.pub_date = timezone.now()209 post.author = author210 post.save()211212 # Check new post saved213 all_posts = Post.objects.all()214 self.assertEquals(len(all_posts), 1)215216 # Fetch the index217 response = self.client.get('/')218 self.assertEquals(response.status_code, 200)219220 # Check the post title is in the response221 self.assertTrue(post.title in response.content)222223 # Check the post text is in the response224 self.assertTrue(markdown.markdown(post.text) in response.content)225226 # Check the post date is in the response227 self.assertTrue(str(post.pub_date.year) in response.content)228 self.assertTrue(post.pub_date.strftime('%b') in response.content)229 self.assertTrue(str(post.pub_date.day) in response.content)230231 # Check the link is marked up properly232 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)233234 def test_post_page(self):235 # Create the author236 author = User.objects.create_user('testuser', 'user@example.com', 'password')237 author.save()238239 # Create the post240 post = Post()241 post.title = 'My first post'242 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'243 post.slug = 'my-first-post'244 post.pub_date = timezone.now()245 post.author = author246 post.save()247248 # Check new post saved249 all_posts = Post.objects.all()250 self.assertEquals(len(all_posts), 1)251 only_post = all_posts[0]252 self.assertEquals(only_post, post)253254 # Get the post URL255 post_url = only_post.get_absolute_url()256257 # Fetch the post258 response = self.client.get(post_url)259 self.assertEquals(response.status_code, 200)260261 # Check the post title is in the response262 self.assertTrue(post.title in response.content)263264 # Check the post text is in the response265 self.assertTrue(markdown.markdown(post.text) in response.content)266267 # Check the post date is in the response268 self.assertTrue(str(post.pub_date.year) in response.content)269 self.assertTrue(post.pub_date.strftime('%b') in response.content)270 self.assertTrue(str(post.pub_date.day) in response.content)271272 # Check the link is marked up properly273 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)274275276class FlatPageViewTest(BaseAcceptanceTest):277 def test_create_flat_page(self):278 # Create flat page279 page = FlatPage()280 page.url = '/about/'281 page.title = 'About me'282 page.content = 'All about me'283 page.save()284285 # Add the site286 page.sites.add(Site.objects.all()[0])287 page.save()288289 # Check new page saved290 all_pages = FlatPage.objects.all()291 self.assertEquals(len(all_pages), 1)292 only_page = all_pages[0]293 self.assertEquals(only_page, page)294295 # Check data correct296 self.assertEquals(only_page.url, '/about/')297 self.assertEquals(only_page.title, 'About me')298 self.assertEquals(only_page.content, 'All about me')299300 # Get URL301 page_url = str(only_page.get_absolute_url())302303 # Get the page304 response = self.client.get(page_url)305 self.assertEquals(response.status_code, 200)306307 # Check title and content in response308 self.assertTrue('About me' in response.content)309 self.assertTrue('All about me' in response.content)
Here we create a User
object to represent the author. Note the create_user
convenience method for creating new users quickly and easily.
We're going to exclude the author field from the admin - instead it's going to be automatically populated based on the session data, so that when a user creates a post they are automatically set as the author. We therefore don't need to make any changes for the acceptance tests for posts - our changes to the unit tests for the Post
model are sufficient.
Run the tests, and they should fail:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test2Creating test database for alias 'default'...3E........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 45, in test_create_post9 self.assertEquals(only_post.author.username, 'testuser')10AttributeError: 'Post' object has no attribute 'author'1112----------------------------------------------------------------------13Ran 9 tests in 3.620s1415FAILED (errors=1)16Destroying test database for alias 'default'...
Let's add the missing author
attribute:
1from django.db import models2from django.contrib.auth.models import User34# Create your models here.5class Post(models.Model):6 title = models.CharField(max_length=200)7 pub_date = models.DateTimeField()8 text = models.TextField()9 slug = models.SlugField(max_length=40, unique=True)10 author = models.ForeignKey(User)1112 def get_absolute_url(self):13 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)1415 def __unicode__(self):16 return self.title1718 class Meta:19 ordering = ["-pub_date"]
Next, create the migrations:
$ python manage.py schemamigration --auto blogengine
You'll be prompted to either quit or provide a default author ID - select option 2 to provide the ID, then enter 1, which should be your own user account ID. Then run the migrations:
$ python manage.py migrate
Let's run our tests again:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test2Creating test database for alias 'default'...3.F.F.....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 118, in test_create_post9 self.assertTrue('added successfully' in response.content)10AssertionError: False is not true1112======================================================================13FAIL: test_edit_post (blogengine.tests.AdminTest)14----------------------------------------------------------------------15Traceback (most recent call last):16 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 154, in test_edit_post17 self.assertTrue('changed successfully' in response.content)18AssertionError: False is not true1920----------------------------------------------------------------------21Ran 9 tests in 3.390s2223FAILED (failures=2)24Destroying test database for alias 'default'...
Our test still fails because the author field isn't set automatically. So we'll amend the admin to automatically set the author when the Post
object is saved:
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.Post, PostAdmin)
This tells the admin to exclude the author
field from any form for a post, and when the model is saved, to set the author to the user making the HTTP request. Now run the tests, and they should pass:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test2Creating test database for alias 'default'...3.........4----------------------------------------------------------------------5Ran 9 tests in 4.086s67OK8Destroying test database for alias 'default'...
Time to commit again:
1$ git add blogengine/2$ git commit -m 'Added author field'
Comments
The previous version of this tutorial implemented comments using Django's own comment system. However, this has since been deprecated from Django and turned into a separate project. So we have two options for how to implement comments:
- We can use the comments system
- We can use a third-party comments system
Now, if you want to use the Django comment system, you can do so, and it shouldn't be too hard to puzzle out how to implement it using the documentation and my prior post. However, in my humble opinion, using a third-party comment system is the way to go for blog comments - they make it extremely easy for people to log in with multiple services without you having to write lots of additional code. They also make it significantly easier to moderate comments, and they're generally pretty good at handling comment spam.
Some of the available providers include:
For demonstration purposes, we'll use Facebook comments, but this shouldn't require much work to adapt it to the other providers.
First of all, we need to include the Facebook JavaScript SDK:
1 <!-- Add your site or application content here -->23 <div id="fb-root"></div>4 <script>(function(d, s, id) {5 var js, fjs = d.getElementsByTagName(s)[0];6 if (d.getElementById(id)) return;7 js = d.createElement(s); js.id = id;8 js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";9 fjs.parentNode.insertBefore(js, fjs);10 }(document, 'script', 'facebook-jssdk'));</script>1112 <div class="navbar navbar-static-top navbar-inverse">13 <div class="navbar-inner">14 <div class="container">15 <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">16 <span class="icon-bar"></span>17 <span class="icon-bar"></span>18 <span class="icon-bar"></span>19 </a>20 <a class="brand" href="/">My Django Blog</a>21 <div class="nav-collapse collapse">22 </div>23 </div>24 </div>25 </div>
Now, the Facebook comment system requires that you pass through the absolute page URL when initialising the comments. At present we can't do that without hard-coding the domain name in our template, which we want to avoid. So, we need to add a site field to each post to identify the site it's associated with.
As usual, we update our tests first:
1from django.test import TestCase, LiveServerTestCase, Client2from django.utils import timezone3from blogengine.models import Post4from django.contrib.flatpages.models import FlatPage5from django.contrib.sites.models import Site6from django.contrib.auth.models import User7import markdown89# Create your tests here.10class PostTest(TestCase):11 def test_create_post(self):12 # Create the author13 author = User.objects.create_user('testuser', 'user@example.com', 'password')14 author.save()1516 # Create the site17 site = Site()18 site.name = 'example.com'19 site.domain = 'example.com'20 site.save()2122 # Create the post23 post = Post()2425 # Set the attributes26 post.title = 'My first post'27 post.text = 'This is my first blog post'28 post.slug = 'my-first-post'29 post.pub_date = timezone.now()30 post.author = author31 post.site = site3233 # Save it34 post.save()3536 # Check we can find it37 all_posts = Post.objects.all()38 self.assertEquals(len(all_posts), 1)39 only_post = all_posts[0]40 self.assertEquals(only_post, post)4142 # Check attributes43 self.assertEquals(only_post.title, 'My first post')44 self.assertEquals(only_post.text, 'This is my first blog post')45 self.assertEquals(only_post.slug, 'my-first-post')46 self.assertEquals(only_post.site.name, 'example.com')47 self.assertEquals(only_post.site.domain, 'example.com')48 self.assertEquals(only_post.pub_date.day, post.pub_date.day)49 self.assertEquals(only_post.pub_date.month, post.pub_date.month)50 self.assertEquals(only_post.pub_date.year, post.pub_date.year)51 self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)52 self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)53 self.assertEquals(only_post.pub_date.second, post.pub_date.second)54 self.assertEquals(only_post.author.username, 'testuser')55 self.assertEquals(only_post.author.email, 'user@example.com')5657class BaseAcceptanceTest(LiveServerTestCase):58 def setUp(self):59 self.client = Client()606162class AdminTest(BaseAcceptanceTest):63 fixtures = ['users.json']6465 def test_login(self):66 # Get login page67 response = self.client.get('/admin/')6869 # Check response code70 self.assertEquals(response.status_code, 200)7172 # Check 'Log in' in response73 self.assertTrue('Log in' in response.content)7475 # Log the user in76 self.client.login(username='bobsmith', password="password")7778 # Check response code79 response = self.client.get('/admin/')80 self.assertEquals(response.status_code, 200)8182 # Check 'Log out' in response83 self.assertTrue('Log out' in response.content)8485 def test_logout(self):86 # Log in87 self.client.login(username='bobsmith', password="password")8889 # Check response code90 response = self.client.get('/admin/')91 self.assertEquals(response.status_code, 200)9293 # Check 'Log out' in response94 self.assertTrue('Log out' in response.content)9596 # Log out97 self.client.logout()9899 # Check response code100 response = self.client.get('/admin/')101 self.assertEquals(response.status_code, 200)102103 # Check 'Log in' in response104 self.assertTrue('Log in' in response.content)105106 def test_create_post(self):107 # Log in108 self.client.login(username='bobsmith', password="password")109110 # Check response code111 response = self.client.get('/admin/blogengine/post/add/')112 self.assertEquals(response.status_code, 200)113114 # Create the new post115 response = self.client.post('/admin/blogengine/post/add/', {116 'title': 'My first post',117 'text': 'This is my first post',118 'pub_date_0': '2013-12-28',119 'pub_date_1': '22:00:04',120 'slug': 'my-first-post',121 'site': '1'122 },123 follow=True124 )125 self.assertEquals(response.status_code, 200)126127 # Check added successfully128 self.assertTrue('added successfully' in response.content)129130 # Check new post now in database131 all_posts = Post.objects.all()132 self.assertEquals(len(all_posts), 1)133134 def test_edit_post(self):135 # Create the author136 author = User.objects.create_user('testuser', 'user@example.com', 'password')137 author.save()138139 # Create the site140 site = Site()141 site.name = 'example.com'142 site.domain = 'example.com'143 site.save()144145 # Create the post146 post = Post()147 post.title = 'My first post'148 post.text = 'This is my first blog post'149 post.slug = 'my-first-post'150 post.pub_date = timezone.now()151 post.author = author152 post.site = site153 post.save()154155 # Log in156 self.client.login(username='bobsmith', password="password")157158 # Edit the post159 response = self.client.post('/admin/blogengine/post/1/', {160 'title': 'My second post',161 'text': 'This is my second blog post',162 'pub_date_0': '2013-12-28',163 'pub_date_1': '22:00:04',164 'slug': 'my-second-post',165 'site': '1'166 },167 follow=True168 )169 self.assertEquals(response.status_code, 200)170171 # Check changed successfully172 self.assertTrue('changed successfully' in response.content)173174 # Check post amended175 all_posts = Post.objects.all()176 self.assertEquals(len(all_posts), 1)177 only_post = all_posts[0]178 self.assertEquals(only_post.title, 'My second post')179 self.assertEquals(only_post.text, 'This is my second blog post')180181 def test_delete_post(self):182 # Create the author183 author = User.objects.create_user('testuser', 'user@example.com', 'password')184 author.save()185186 # Create the site187 site = Site()188 site.name = 'example.com'189 site.domain = 'example.com'190 site.save()191192 # Create the post193 post = Post()194 post.title = 'My first post'195 post.text = 'This is my first blog post'196 post.slug = 'my-first-post'197 post.pub_date = timezone.now()198 post.site = site199 post.author = author200 post.save()201202 # Check new post saved203 all_posts = Post.objects.all()204 self.assertEquals(len(all_posts), 1)205206 # Log in207 self.client.login(username='bobsmith', password="password")208209 # Delete the post210 response = self.client.post('/admin/blogengine/post/1/delete/', {211 'post': 'yes'212 }, follow=True)213 self.assertEquals(response.status_code, 200)214215 # Check deleted successfully216 self.assertTrue('deleted successfully' in response.content)217218 # Check post amended219 all_posts = Post.objects.all()220 self.assertEquals(len(all_posts), 0)221222class PostViewTest(BaseAcceptanceTest):223 def test_index(self):224 # Create the author225 author = User.objects.create_user('testuser', 'user@example.com', 'password')226 author.save()227228 # Create the site229 site = Site()230 site.name = 'example.com'231 site.domain = 'example.com'232 site.save()233234 # Create the post235 post = Post()236 post.title = 'My first post'237 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'238 post.slug = 'my-first-post'239 post.pub_date = timezone.now()240 post.author = author241 post.site = site242 post.save()243244 # Check new post saved245 all_posts = Post.objects.all()246 self.assertEquals(len(all_posts), 1)247248 # Fetch the index249 response = self.client.get('/')250 self.assertEquals(response.status_code, 200)251252 # Check the post title is in the response253 self.assertTrue(post.title in response.content)254255 # Check the post text is in the response256 self.assertTrue(markdown.markdown(post.text) in response.content)257258 # Check the post date is in the response259 self.assertTrue(str(post.pub_date.year) in response.content)260 self.assertTrue(post.pub_date.strftime('%b') in response.content)261 self.assertTrue(str(post.pub_date.day) in response.content)262263 # Check the link is marked up properly264 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)265266 def test_post_page(self):267 # Create the author268 author = User.objects.create_user('testuser', 'user@example.com', 'password')269 author.save()270271 # Create the site272 site = Site()273 site.name = 'example.com'274 site.domain = 'example.com'275 site.save()276277 # Create the post278 post = Post()279 post.title = 'My first post'280 post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'281 post.slug = 'my-first-post'282 post.pub_date = timezone.now()283 post.author = author284 post.site = site285 post.save()286287 # Check new post saved288 all_posts = Post.objects.all()289 self.assertEquals(len(all_posts), 1)290 only_post = all_posts[0]291 self.assertEquals(only_post, post)292293 # Get the post URL294 post_url = only_post.get_absolute_url()295296 # Fetch the post297 response = self.client.get(post_url)298 self.assertEquals(response.status_code, 200)299300 # Check the post title is in the response301 self.assertTrue(post.title in response.content)302303 # Check the post text is in the response304 self.assertTrue(markdown.markdown(post.text) in response.content)305306 # Check the post date is in the response307 self.assertTrue(str(post.pub_date.year) in response.content)308 self.assertTrue(post.pub_date.strftime('%b') in response.content)309 self.assertTrue(str(post.pub_date.day) in response.content)310311 # Check the link is marked up properly312 self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)313314315class FlatPageViewTest(BaseAcceptanceTest):316 def test_create_flat_page(self):317 # Create flat page318 page = FlatPage()319 page.url = '/about/'320 page.title = 'About me'321 page.content = 'All about me'322 page.save()323324 # Add the site325 page.sites.add(Site.objects.all()[0])326 page.save()327328 # Check new page saved329 all_pages = FlatPage.objects.all()330 self.assertEquals(len(all_pages), 1)331 only_page = all_pages[0]332 self.assertEquals(only_page, page)333334 # Check data correct335 self.assertEquals(only_page.url, '/about/')336 self.assertEquals(only_page.title, 'About me')337 self.assertEquals(only_page.content, 'All about me')338339 # Get URL340 page_url = str(only_page.get_absolute_url())341342 # Get the page343 response = self.client.get(page_url)344 self.assertEquals(response.status_code, 200)345346 # Check title and content in response347 self.assertTrue('About me' in response.content)348 self.assertTrue('All about me' in response.content)
All we've done here is to add the site
attribute when creating a new post using the Django database API, and when we create one via the admin, we add an additional site
parameter to the HTTP POST request with a value of 1. Run the tests and they should fail:
1Creating test database for alias 'default'...2E........3======================================================================4ERROR: test_create_post (blogengine.tests.PostTest)5----------------------------------------------------------------------6Traceback (most recent call last):7 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 46, in test_create_post8 self.assertEquals(only_post.site.name, 'example.com')9AttributeError: 'Post' object has no attribute 'site'1011----------------------------------------------------------------------12Ran 9 tests in 4.313s1314FAILED (errors=1)15Destroying test database for alias 'default'...
So we need to add the site
attribute to the Post
model. 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 Post(models.Model):7 title = models.CharField(max_length=200)8 pub_date = models.DateTimeField()9 text = models.TextField()10 slug = models.SlugField(max_length=40, unique=True)11 author = models.ForeignKey(User)12 site = models.ForeignKey(Site)1314 def get_absolute_url(self):15 return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)1617 def __unicode__(self):18 return self.title1920 class Meta:21 ordering = ["-pub_date"]
Now create and run the migrations - you'll be prompted to create a default value for the site attribute as well:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py schemamigration --auto blogengine2 ? The field 'Post.site' does not have a default specified, yet is NOT NULL.3 ? Since you are adding this field, you MUST specify a default4 ? value to use for existing rows. Would you like to:5 ? 1. Quit now, and add a default to the field in models.py6 ? 2. Specify a one-off value to use for existing columns now7 ? Please select a choice: 28 ? Please enter Python code for your one-off default value.9 ? The datetime module is available, so you can do e.g. datetime.date.today()10 >>> 111 + Added field site on blogengine.Post12Created 0005_auto__add_field_post_site.py. You can now apply this migration with: ./manage.py migrate blogengine13(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py migrate14Running migrations for blogengine:15 - Migrating forwards to 0005_auto__add_field_post_site.16 > blogengine:0005_auto__add_field_post_site17 - Loading initial data for blogengine.18Installed 0 object(s) from 0 fixture(s)
Our tests should then pass:
1(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test2Creating test database for alias 'default'...3.........4----------------------------------------------------------------------5Ran 9 tests in 4.261s67OK8Destroying test database for alias 'default'...
Now we can include our full page URL on the 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 }}1011 <h4>Comments</h4>12 <div class="fb-comments" data-href="http://{{ post.site }}{{ post.get_absolute_url }}" data-width="470" data-num-posts="10"></div>1314 </div>1516 {% endblock %}
If you want to customise the comments, take a look at the documentation for Facebook Comments.
With that done, we can commit our changes:
1$ git add blogengine/ templates/2$ git commit -m 'Implemented Facebook comments'
And that wraps up this lesson. As usual, you can easily switch to today's lesson with git checkout lesson-3
. Next time we'll implement categories and tags, and create an RSS feed for our blog posts.