What's new in Django community blogs?

Django's Inaugural Technical Board Elected

Sep 07 2014 [Archived Version] □ Published at The Django weblog

Following the recent changes to the structure of the Django team's organization, I'm pleased to make this announcement as the returns officer. The team has elected the inaugural technical board:

  • Aymeric Augustin
  • Andrew Godwin
  • Russell Keith-Magee
  • Jannis Leidel
  • Carl Meyer

The technical board is a group of experienced and active committers who steer technical choices. Their main concern is to maintain the quality and stability of the Django Web Framework.

Thank-you to the candidates for their past service, and we look forward to their future leadership.


Security on a Django app - HTTPS everywhere

Sep 04 2014 [Archived Version] □ Published at Python, Science and Marketing under tags  django heroku security


Authentication credentials between your site and your end users must be secret. Is your site secure enough?

One important step is to use HTTPS everywhere in your Django website, and in this post we will show you how to implement it.

In particular, if you are deploying your App on Heroku, don't miss this post ;-)

From HTTP to HTTPS

Fortunately, all Heroku app domains, like myapp.herokuapp.com, are SSL enabled and can be accessed via https. If you are redirecting the Heroku app to your domain, the configuration can be more complicated -- better check the official documentation.

If you maintain the Heroku domain, we just need to redirect all the http requests to be https.

For that matter, we will use the package django-secure, which helps you configure and check some security aspects.

Activate your development environment and install the package with:

$ workon dev_env
$ pip install django-secure

Check the version installed with

$ pip freeze

and add the package in the requirements/common.txt file (as this package will be used for the testing, developing and production environments). We are following the requirements structure of the tutorials in this website, which consists in a common.txt file containing all the packages that are used for all the environments, and a separate file for each environment: test.txt, dev.txt and prod.txt. These files inherit from the common.txt file with the command -r common.txt.

Next we can install the same package with in your testing environment with

$ workon test_env
$ pip install -r requirements/test.txt

Once installed, add it into the settings.py file (if you are using different setting files for each environment, install it in your common settings file):

INSTALLED_APPS = (
    ...
    'djangosecure',
    ...
)

and add the corresponding middleware (make sure to add it at the beginning, so that it's the first thing it sees):

MIDDLEWARE_CLASSES = (
    'djangosecure.middleware.SecurityMiddleware',
    ...
)

Finally, define the variable:

SECURE_SSL_REDIRECT = True

this will redirect all non-SSL requests to SSL (http to https). If you have a infinite redirects, check this post in the django-secure documentation to solve it. If you are using Heroku though, keep reading.

SSL Django Server

If you want to check if the SSL redirect is working on your local computer, you can install another package, named django-sslserver, that provides an SSL-enabled development server.

$ workon dev_env
$ pip install django-sslserver

Next, check the version installed and add it into the requirements/dev.txt file (not in the common.txt file!). Do the same with the testing environment.

Add it into installed apps, only in development_settings.py and testing_settings.py with:

INSTALLED_APPS += (
    ...
    'sslserver',
    ...
)

Note the += in the statement. This adds the 'sslserver' app to the installed apps defined in the common settings.py file.

Now you can run the SSL enabled server with

$ python manage.py runsslserver

If you have infinite redirects on your local server, you can try to solve it by checking this post in the django-secure documentation. However, I will take another approach.

I will just remove the SSL redirect on both testing and development settings files:

SECURE_SSL_REDIRECT = False

This way, both http and https will be available (one with runserver and the other with runsslserver). If this doesn't work for you, it might be because of the cache in your browser. Try to access the localhost with a private window (on Mac, type cmd+shift+n to open one).

If you go to your localhost while running the SSL server, you'll see that you've been redirected to the https version. However, your browser will complain because the SSL certificate is home-made and the browser doesn't trust it.

HTTPS on Heroku

For the particular case of Heroku, you need to add the following setting to avoid the infinite redirects.

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

This is because Heroku sets the request header to HTTP_X_FORWARDED_PROTO when the request comes via an https.

Ok, commit your changes and push them into Heroku. Does it work?

Secure Cookies

Sometimes browser send cookies back to the server. We should make sure that this transfer is made under an HTTPS request.

Open again the common settings.py and add the following:

SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

But change them to false in your development and testing settings. Otherwise, you won't be able to login or send a form with the development server.

HTTP Strict Transport Security

Moreover, django-secure allows you to set the Strict Transport Security header. As I understand, your server tells each browser to only access your site via HTTPS. Therefore, when a user tries to go to your site with an HTTP, the browser changes to HTTPS and makes the request. Note that it is the browser working, not your server!

This prevents attacks from man-in-the-middle, as the ability to intercept requests and responses between the user and your server has been greatly reduced.

To activate HSTS, go to your settings.py file and add

SECURE_HSTS_SECONDS = 31536000 

which indicates browsers that future requests for the next year should use only HTTPS.

If your site has subdomains defined, you may want to add them the header too. You just need:

SECURE_HSTS_INCLUDE_SUBDOMAINS = True

Check your Django Site Security

It's time to check the security of your Django site! You can do it here:


Can you get a 100% rate? ;-)


Want to learn more about Django?

Give it a +1 if you liked it, and share it with your friends! :-)



Django 1.7 released

Sep 02 2014 [Archived Version] □ Published at The Django weblog

It's here!

After nearly a year of development, today we're proud to announce the release of Django 1.7.

As always, the release notes cover everything in-depth, but some of the major highlights of Django 1.7 are:

  • A new built-in database migration system. Notes on upgrading from South (a popular third-party application providing migration functionality) are also available.
  • A refactored concept of Django applications. Django applications are no longer tied to the existence of a models files, and can now specify both configuration data and code to be executed as Django starts up.
  • Improvements to the model Field API to support migrations and, in the future, to enable easy addition of composite-key support to Django's ORM.
  • Improvements for custom Manager and QuerySet classes, allowing reverse relationship traversal to specify the Manager to use, and creation of a Manager from a custom QuerySet class.
  • A extensible system check framework which can assist developers in detecting and diagnosing errors.

And that's just the tip of the iceberg; Django 1.7 is jam-packed with useful new features and functionality, and they're all covered in the 1.7 release notes.

You can get Django 1.7 right now from our downloads page (along with checksums), or from the Python Package Index.

Bugfix releases

Alongside Django 1.7, today we are issuing bugfix releases in the 1.4, 1.5 and 1.6 series, which correct some bugs which existed after the most recent security releases.

Today's releases are:

Also, with the release of Django 1.7, Django 1.5 has reached end-of-life. As such, Django 1.5.10 is the final release of the 1.5 series. Django 1.6 will continue to receive support until the release of Django 1.8. Django 1.4 is a long-term support release, and will be supported until at least March of 2015.


Django Blog Tutorial - the Next Generation - Part 8

Aug 31 2014 [Archived Version] □ Published at Matthew Daly's Blog

Hello again! In our final instalment, we’ll wrap up our blog by:

  • Implementing a sitemap
  • Optimising and tidying up the site
  • Creating a Fabric task for easier deployment

I’ll also cover development tools and practices that can make using Django easier. But first there’s a few housekeeping tasks that need doing…

Don’t forget to activate your virtualenv - you should know how to do this off by heart by now!

Upgrading Django

At the time of writing, Django 1.7 is due any day now, but it’s not out yet so I won’t cover it. The biggest change is the addition of a built-in migration system, but switching from South to this is well-documented. When Django 1.7 comes out, it shouldn’t be difficult to upgrade to it - because we have good test coverage, we shouldn’t have much trouble catching errors.

However, Django 1.6.6 was recently released, and we need to upgrade to it. Just enter the following command to upgrade:

pip install Django --upgrade

Then add it to your requirements.txt:

pip freeze > requirements.txt

Then commit your changes:

git add requirements.txt
git commit -m 'Upgraded Django version'

Implementing a sitemap

Creating a sitemap for your blog is a good idea - it can be submitted to search engines, so that they can easily find your content. With Django, it’s pretty straightforward too.

First, let’s create a test for our sitemap. Add the following code at the end of tests.py:

class SitemapTest(BaseAcceptanceTest):
    def test_sitemap(self):
        # Create a post
        post = PostFactory()

        # Create a flat page
        page = FlatPageFactory()

        # Get sitemap
        response = self.client.get('/sitemap.xml')
        self.assertEquals(response.status_code, 200)

        # Check post is present in sitemap
        self.assertTrue('my-first-post' in response.content)

        # Check page is present in sitemap
        self.assertTrue('/about/' in response.content)

Run it, and you should see the test fail:

$ python manage.py test blogengine
Creating test database for alias 'default'...
...........................F
======================================================================
FAIL: test_sitemap (blogengine.tests.SitemapTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 847, in test_sitemap
    self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200

----------------------------------------------------------------------
Ran 28 tests in 6.873s

FAILED (failures=1)
Destroying test database for alias 'default'...

Now, let’s implement our sitemap. The sitemap application comes with Django, and needs to be activated in your settings file, under INSTALLED_APPS:

    'django.contrib.sitemaps',

Next, let’s think about what content we want to include in the sitemap. We want to index our flat pages and our blog posts, so our sitemap should reflect that. Create a new file at blogengine/sitemap.py and enter the following text:

from django.contrib.sitemaps import Sitemap
from django.contrib.flatpages.models import FlatPage
from blogengine.models import Post

class PostSitemap(Sitemap):
    changefreq = "always"
    priority = 0.5

    def items(self):
        return Post.objects.all()

    def lastmod(self, obj):
        return obj.pub_date


class FlatpageSitemap(Sitemap):
    changefreq = "always"
    priority = 0.5

    def items(self):
        return FlatPage.objects.all()

We define two sitemaps, one for all the posts, and the other for all the flat pages. Note that this works in a very similar way to the syndication framework.

Next, we amend our URLs. Add the following text after the existing imports in your URL file:

from django.contrib.sitemaps.views import sitemap
from blogengine.sitemap import PostSitemap, FlatpageSitemap

# Define sitemaps
sitemaps = {
    'posts': PostSitemap,
    'pages': FlatpageSitemap
}

Then add the following after the existing routes:

    # Sitemap
    url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
            name='django.contrib.sitemaps.views.sitemap'),

Here we define what sitemaps we’re going to use, and we define a URL for them. It’s pretty straightforward to use.

Let’s run our tests:

$ python manage.py test blogengine
Creating test database for alias 'default'...
............................
----------------------------------------------------------------------
Ran 28 tests in 6.863s

OK
Destroying test database for alias 'default'...

And done! Let’s commit our changes:

git add blogengine/ django_tutorial_blog_ng/settings.py
git commit -m 'Implemented a sitemap'

Fixing test coverage

Our blog is now feature-complete, but there are a few gaps in test coverage, so we’ll fix them. If, like me, you’re using Coveralls.io, you can easily see via their web interface where there are gaps in the coverage.

Now, our gaps are all in our view file - if you take a look at my build, you can easily identify the gaps as they’re marked in red.

The first gap is where a tag does not exist. Interestingly, if we look at the code in the view, we can see that some of it is redundant:

class TagPostsFeed(PostsFeed):
    def get_object(self, request, slug):
        return get_object_or_404(Tag, slug=slug)

    def title(self, obj):
        return "RSS feed - blog posts tagged  %s" % obj.name

    def link(self, obj):
        return obj.get_absolute_url()

    def description(self, obj):
        return "RSS feed - blog posts tagged %s" % obj.name

    def items(self, obj):
        try:
            tag = Tag.objects.get(slug=obj.slug)
            return tag.post_set.all()
        except Tag.DoesNotExist:
            return Post.objects.none()

Under the items function, we check to see if the tag exists. However, under get_object we can see that if the object didn’t exist, it would already have returned a 404 error. We can therefore safely amend items to not check, since that try statement will never fail:

class TagPostsFeed(PostsFeed):
    def get_object(self, request, slug):
        return get_object_or_404(Tag, slug=slug)

    def title(self, obj):
        return "RSS feed - blog posts tagged  %s" % obj.name

    def link(self, obj):
        return obj.get_absolute_url()

    def description(self, obj):
        return "RSS feed - blog posts tagged %s" % obj.name

    def items(self, obj):
        tag = Tag.objects.get(slug=obj.slug)
        return tag.post_set.all()

The other two gaps are in our search view - we never get an empty result for the search in the following section:

def getSearchResults(request):
    """
    Search for a post by title or text
    """
    # Get the query data
    query = request.GET.get('q', '')
    page = request.GET.get('page', 1)

    # Query the database
    if query:
        results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
    else:
        results = None

    # Add pagination
    pages = Paginator(results, 5)

    # Get specified page
    try:
        returned_page = pages.page(page)
    except EmptyPage:
        returned_page = pages.page(pages.num_pages)

    # Display the search results
    return render_to_response('blogengine/search_post_list.html',
                              {'page_obj': returned_page,
                               'object_list': returned_page.object_list,
                               'search': query})

So replace it with this:

def getSearchResults(request):
    """
    Search for a post by title or text
    """
    # Get the query data
    query = request.GET.get('q', '')
    page = request.GET.get('page', 1)

    # Query the database
    results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))

    # Add pagination
    pages = Paginator(results, 5)

    # Get specified page
    try:
        returned_page = pages.page(page)
    except EmptyPage:
        returned_page = pages.page(pages.num_pages)

    # Display the search results
    return render_to_response('blogengine/search_post_list.html',
                              {'page_obj': returned_page,
                               'object_list': returned_page.object_list,
                               'search': query})

We don’t need to check whether query is defined because if q is left blank, the value of query will be an empty string, so we may as well pull out the redundant code.

Finally, the other gap is for when a user tries to get an empty search page (eg, page two of something with five or less results). So let’s add another test to our SearchViewTest class:

    def test_failing_search(self):
        # Search for something that is not present
        response = self.client.get('/search?q=wibble')
        self.assertEquals(response.status_code, 200)
        self.assertTrue('No posts found' in response.content)

        # Try to get nonexistent second page
        response = self.client.get('/search?q=wibble&page=2')
        self.assertEquals(response.status_code, 200)
        self.assertTrue('No posts found' in response.content)

Run our tests and check the coverage:

coverage run --include="blogengine/*" --omit="blogengine/migrations/*" manage.py test blogengine
coverage html

If you open htmlcov/index.html in your browser, you should see that the test coverage is back up to 100%. With that done, it’s time to commit again:

git add blogengine/
git commit -m 'Fixed gaps in coverage'

Remember, it’s not always possible to achieve 100% test coverage, and you shouldn’t worry too much about it if it’s not possible - it’s possible to ignore code if necessary. However, it’s a good idea to aim for 100%.

Using Fabric for deployment

Next we’ll cover using Fabric, a handy tool for deploying your changes (any pretty much any other task you want to automate). First, you need to install it:

pip install Fabric

If you have any problems installing it, you should be able to resolve them via Google - most of them are likely to be absent libraries that Fabric depends upon. Once it’s installed, add it to your requirements.tzt:

pip freeze > requirements.txt

Next, create a file called fabfile.py and enter the following text:

#!/usr/bin/env python
from fabric.api import local

def deploy():
    """
    Deploy the latest version to Heroku
    """
    # Push changes to master
    local("git push origin master")

    # Push changes to Heroku
    local("git push heroku master")

    # Run migrations on Heroku
    local("heroku run python manage.py migrate")

Now, all this file does is push our changes to Github (or wherever else your repository is hosted) and to Heroku, and runs your migrations. It’s not a terribly big task anyway, but it’s handy to have it in place. Let’s commit our changes:

git add fabfile.py requirements.txt
git commit -m 'Added Fabric task for deployment'

Then, let’s try it out:

$ fab deploy

There, wasn’t that more convenient? Fabric is much more powerful than this simple demonstration indicates, and can run tasks on remote servers via SSH easily. I recommend you take a look at the documentation to see what else you can do with it. If you’re hosting your site on a VPS, you will probably find Fabric indispensable, as you will need to restart the application every time you push up a new revision.

Tidying up

We want our blog application to play nicely with other Django apps. For instance, say you’re working on a new site that includes a blogging engine. Wouldn’t it make sense to just be able to drop in this blogging engine and have it work immediately? At the moment, some of our URL’s are hard-coded, so we may have problems in doing so. Let’s fix that.

First we’ll amend our tests. Add this at the top of the tests file:

from django.core.urlresolvers import reverse

Next, replace every instance of this:

        response = self.client.get('/')

with this:

response = self.client.get(reverse('blogengine:index'))

Then, rewrite the calls to the search route. For instance, this:

        response = self.client.get('/search?q=first')

should become this:

        response = self.client.get(reverse('blogengine:search') + '?q=first')

I’ll leave changing these as an exercise for the reader, but check the repository if you get stuck.

Next, we need to assign a namespace to our app’s routes:

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'django_tutorial_blog_ng.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),

    # Blog URLs
    url(r'', include('blogengine.urls', namespace="blogengine")),

    # Flat pages
    url(r'', include('django.contrib.flatpages.urls')),
)

We then assign names to our routes in the app’s urls.py:

from django.conf.urls import patterns, url
from django.views.generic import ListView, DetailView
from blogengine.models import Post, Category, Tag
from blogengine.views import CategoryListView, TagListView, PostsFeed, CategoryPostsFeed, TagPostsFeed, getSearchResults
from django.contrib.sitemaps.views import sitemap
from blogengine.sitemap import PostSitemap, FlatpageSitemap

# Define sitemaps
sitemaps = {
    'posts': PostSitemap,
    'pages': FlatpageSitemap
}

urlpatterns = patterns('',
    # Index
    url(r'^(?P<page>\d+)?/?$', ListView.as_view(
        model=Post,
        paginate_by=5,
        ),
        name='index'
        ),

    # Individual posts
    url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
        model=Post,
        ),
        name='post'
        ),

    # Categories
    url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
        paginate_by=5,
        model=Category,
        ),
        name='category'
        ),


    # Tags
    url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
        paginate_by=5,
        model=Tag,
        ),
        name='tag'
        ),

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

    # Category RSS feed
    url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),

    # Tag RSS feed
    url(r'^feeds/posts/tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagPostsFeed()),

    # Search posts
    url(r'^search', getSearchResults, name='search'),

    # Sitemap
    url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
            name='django.contrib.sitemaps.views.sitemap'),
)

You also need to amend two of your templates:

``` html blogengine/templates/blogengine/includes/base.html {% raw %}<!DOCTYPE html>

{% block title %}My Django Blog{% endblock %}

    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->

    {% load staticfiles %}
    <link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/normalize.css' %}">
    <link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/main.css' %}">
    <link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap-theme.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    <link rel="stylesheet" href="{% static 'css/code.css' %}">
    <script src="{% static 'bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js' %}"></script>
</head>
<body>
    <!--[if lt IE 7]>
        <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
    <![endif]-->

    <!-- Add your site or application content here -->

    <div id="fb-root"></div>
    <script>(function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) return;
            js = d.createElement(s); js.id = id;
            js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));</script>

    <div class="navbar navbar-static-top navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="{% url 'blogengine:index' %}">My Django Blog</a>
            </div>
            <div class="collapse navbar-collapse" id="header-nav">
                <ul class="nav navbar-nav">
                    {% load flatpages %}
                    {% get_flatpages as flatpages %}
                    {% for flatpage in flatpages %}
                    <li><a href="{{ flatpage.url }}">{{ flatpage.title }}</a></li>
                    {% endfor %}
                    <li><a href="http://matthewdaly.co.uk/feeds/posts/">RSS feed</a></li>

                    <form action="/search" method="GET" class="navbar-form navbar-left">
                        <div class="form-group">
                            <input type="text" name="q" placeholder="Search..." class="form-control"></input>
                        </div>
                        <button type="submit" class="btn btn-default">Search</button>
                    </form>
                </ul>
            </div>
        </div>
    </div>

    <div class="container">
        {% block header %}
            <div class="page-header">
                <h1>My Django Blog</h1>
            </div>
        {% endblock %}

        <div class="row">
            {% block content %}{% endblock %}
        </div>
    </div>

    <div class="container footer">
        <div class="row">
            <div class="span12">
                <p>Copyright &copy; {% now "Y" %}</p>
            </div>
        </div>
    </div>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    <script>window.jQuery || document.write('<script src="{% static 'bower_components/html5-boilerplate/js/vendor/jquery-1.10.2.min.js' %}"><\/script>')</script>
    <script src="{% static 'bower_components/html5-boilerplate/js/plugins.js' %}"></script>
    <script src="{% static 'bower_components/bootstrap/dist/js/bootstrap.min.js' %}"></script>

    <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
    <script>
        (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
        function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
        e=o.createElement(i);r=o.getElementsByTagName(i)[0];
        e.src='//www.google-analytics.com/analytics.js';
        r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
        ga('create','UA-XXXXX-X');ga('send','pageview');
    </script>
</body>

{% endraw %} ```

{% raw %}{% extends "blogengine/includes/base.html" %}

    {% load custom_markdown %}

    {% block content %}
        {% if object_list %}
            {% for post in object_list %}
            <div class="post col-md-12">
            <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
            <h3>{{ post.pub_date }}</h3>
            {{ post.text|custom_markdown }}
            </div>
            {% if post.category %}
            <div class="col-md-12">
            <a href="{{ post.category.get_absolute_url }}"><span class="label label-primary">{{ post.category.name }}</span></a>
            </div>
            {% endif %}
            {% if post.tags %}
            <div class="col-md-12">
            {% for tag in post.tags.all %}
            <a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
            {% endfor %}
            </div>
            {% endif %}
            {% endfor %}
        {% else %}
            <p>No posts found</p>
        {% endif %}

        <ul class="pager">
        {% if page_obj.has_previous %}
        <li class="previous"><a href="{% url 'blogengine:search' %}?page={{ page_obj.previous_page_number }}&q={{ search }}">Previous Page</a></li>
        {% endif %}
        {% if page_obj.has_next %}
        <li class="next"><a href="{% url 'blogengine:search' %}?page={{ page_obj.next_page_number }}&q={{ search }}">Next Page</a></li>
        {% endif %}
        </ul>

    {% endblock %}{% endraw %}

Let’s run our tests:

$ python manage.py test blogengine/
Creating test database for alias 'default'...
.............................
----------------------------------------------------------------------
Ran 29 tests in 10.456s

OK
Destroying test database for alias 'default'...

And commit our changes:

git add .
git commit -m 'Now use named routes'

Debugging Django

There are a number of handy ways to debug Django applications. One of the simplest is to use the Python debugger. To use it, just enter the following lines at the point you want to break at:

import pdb
pdb.set_trace()

Now, whenever that line of code is run, you’ll be dropped into an interactive shell that lets you play around to find out what’s going wrong. However, it doesn’t offer autocompletion, so we’ll install ipdb, which is an improved version:

pip install ipdb
pip freeze > requirements.txt

Now you can use ipdb in much the same way as you would use pdb:

import ipdb
ipdb.set_trace()

Now, ipdb is very useful, but it isn’t much help for profiling your application. For that you need the Django Debug Toolbar. Run the following commands:

pip install django-debug-toolbar
pip freeze > requirements.txt

Then add the following line to INSTALLED_APPS in your settings file:

    'debug_toolbar',

Then, try running the development server, and you’ll see a toolbar on the right-hand side of the screen that allows you to view some useful data about your page. For instance, you’ll notice a field called SQL - this contains details of the queries carried out when building the page. To actually see the queries carried out, you’ll want to disable caching in your settings file by commenting out all the constants that start with CACHE.

We won’t go into using the toolbar to optimise queries, but using this, you can easily see what queries are being executed on a specific page, how long they take, and the values they return. Sometimes, you may need to optimise a slow query - in this case, Django allows you to drop down to writing raw SQL if necessary.

Note that if you’re running Django in production, you should set DEBUG to False as otherwise it gives rather too much information to potential attackers, and with Django Debug Toolbar installed, that’s even more important.

Please also note that when you disable debug mode, Django no longer handles static files automatically, so you’ll need to run python manage.py collectstatic and commit the staticfiles directory.

Once you’ve disabled debug mode, collected the static files, and re-enables caching, you can commit your changes:

git add .
git commit -m 'Installed debugging tools'

Optimising static files

We want our blog to get the best SEO results it can, so making it fast is essential. One of the simplest things you can do is to concatenate and minify static assets such as CSS and JavaScript. There are numerous ways to do this, but I generally use Grunt. Let’s set up a Grunt config to concatenate and minify our CSS and JavaScript.

You’ll need to have Node.js installed on your development machine for this. Then, you need to install the Grunt command-line interface:

sudo npm install -g grunt-cli

With that done, we need to create a package.json file. You can create one using the command npm init. Here’s mine:

{
  "name": "django_tutorial_blog_ng",
  "version": "1.0.0",
  "description": "Django Tutorial Blog NG =======================",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/matthewbdaly/django_tutorial_blog_ng.git"
  },
  "author": "Matthew Daly <matthew@matthewdaly.co.uk> (http://matthewdaly.co.uk/)",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/matthewbdaly/django_tutorial_blog_ng/issues"
  },
  "homepage": "https://github.com/matthewbdaly/django_tutorial_blog_ng"
}

Feel free to amend it as you see fit.

Next we install Grunt and the required plugins:

npm install grunt grunt-contrib-cssmin grunt-contrib-concat grunt-contrib-uglify --save-dev

We now need to create a Gruntfile for our tasks:

module.exports = function (grunt) {
    'use strict';

    grunt.initConfig({
        concat: {
            dist: {
                src: [
                    'blogengine/static/bower_components/bootstrap/dist/css/bootstrap.css',
                    'blogengine/static/bower_components/bootstrap/dist/css/bootstrap-theme.css',
                    'blogengine/static/css/code.css',
                    'blogengine/static/css/main.css',
                ],
                dest: 'blogengine/static/css/style.css'
            }
        },
        uglify: {
            dist: {
                src: [
                    'blogengine/static/bower_components/jquery/jquery.js',
                    'blogengine/static/bower_components/bootstrap/dist/js/bootstrap.js'
                ],
                dest: 'blogengine/static/js/all.min.js'
            }
        },
        cssmin: {
            dist: {
                src: 'blogengine/static/css/style.css',
                dest: 'blogengine/static/css/style.min.css'
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.registerTask('default', ['concat', 'uglify', 'cssmin']);
};

You’ll also need to change the paths in your base HTML file to point to the minified versions:

``` html blogengine/templates/blogengine/includes/base.html {% raw %}<!DOCTYPE html>

{% block title %}My Django Blog{% endblock %}

    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->

    {% load staticfiles %}
    <link rel="stylesheet" href="{% static 'css/style.min.css' %}">
</head>
<body>
    <!--[if lt IE 7]>
        <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
    <![endif]-->

    <!-- Add your site or application content here -->

    <div id="fb-root"></div>
    <script>(function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) return;
            js = d.createElement(s); js.id = id;
            js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));</script>

    <div class="navbar navbar-static-top navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="{% url 'blogengine:index' %}">My Django Blog</a>
            </div>
            <div class="collapse navbar-collapse" id="header-nav">
                <ul class="nav navbar-nav">
                    {% load flatpages %}
                    {% get_flatpages as flatpages %}
                    {% for flatpage in flatpages %}
                    <li><a href="{{ flatpage.url }}">{{ flatpage.title }}</a></li>
                    {% endfor %}
                    <li><a href="http://matthewdaly.co.uk/feeds/posts/">RSS feed</a></li>

                    <form action="/search" method="GET" class="navbar-form navbar-left">
                        <div class="form-group">
                            <input type="text" name="q" placeholder="Search..." class="form-control"></input>
                        </div>
                        <button type="submit" class="btn btn-default">Search</button>
                    </form>
                </ul>
            </div>
        </div>
    </div>

    <div class="container">
        {% block header %}
            <div class="page-header">
                <h1>My Django Blog</h1>
            </div>
        {% endblock %}

        <div class="row">
            {% block content %}{% endblock %}
        </div>
    </div>

    <div class="container footer">
        <div class="row">
            <div class="span12">
                <p>Copyright &copy; {% now "Y" %}</p>
            </div>
        </div>
    </div>

    <script src="{% static 'js/all.min.js' %}"></script>

    <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
    <script>
        (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
        function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
        e=o.createElement(i);r=o.getElementsByTagName(i)[0];
        e.src='//www.google-analytics.com/analytics.js';
        r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
        ga('create','UA-XXXXX-X');ga('send','pageview');
    </script>
</body>

{% endraw %} ```

Now, run the Grunt task:

grunt

And collect the static files:

python manage.py collectstatic

You’ll also want to add your node_modules folder to your gitignore:

venv/
*.pyc
db.sqlite3
reports/
htmlcov/
.coverage
node_modules/

Then commit your changes:

git add .
git commit -m 'Optimised static assets'

Now, our package.json will cause a problem - it will mean that this app is mistakenly identified as a Node.js app. To prevent this, create the following file:

package.json

Then commit your changes and push them up:

git add .slugignore
git commit -m 'Added slugignore'
fab deploy

If you check, your site should now be loading the minified versions of the static files.

That’s our site done! As usual I’ve tagged the final commit with lesson-8.

Sadly, that’s our final instalment over with! I hope you’ve enjoyed these tutorials, and I look forward to seeing what you create with them.


Setting up OpenSprinkler

Aug 28 2014 [Archived Version] □ Published at Nerdy Dork under tags  technology

I set up my OpenSprinkler last night. It was a pretty painless endeavor. I’ll walk through it in this post so I can explain what I did. The Purchase There are a few different models and options to choose from on RaysHobby.net. Time seems to be my greatest commodity at this stage in life so […]

The post Setting up OpenSprinkler appeared first on Dustin Davis.


eGenix PyRun - One file Python Runtime 2.0.1 GA

Aug 27 2014 [Archived Version] □ Published at eGenix.com News & Events

Introduction

eGenix PyRun is our open source, one file, no installation version of Python, making the distribution of a Python interpreter to run based scripts and applications to Unix based systems as simple as copying a single file.

eGenix PyRun's executable only needs 11MB for Python 2 and 13MB for Python 3, but still supports most Python application and scripts - and it can be compressed to just 3-4MB using upx, if needed.

Compared to a regular Python installation of typically 100MB on disk, eGenix PyRun is ideal for applications and scripts that need to be distributed to several target machines, client installations or customers.

It makes "installing" Python on a Unix based system as simple as copying a single file.

eGenix has been using the product internally in the mxODBC Connect Server since 2008 with great success and decided to make it available as a stand-alone open-source product.

We provide both the source archive to build your own eGenix PyRun, as well as pre-compiled binaries for Linux, FreeBSD and Mac OS X, as 32- and 64-bit versions. The binaries can be downloaded manually, or you can let our automatic install script install-pyrun take care of the installation: ./install-pyrun dir and you're done.

Please see the product page for more details:

    >>> eGenix PyRun - One file Python Runtime

News

This is a patch level release of eGenix PyRun 2.0. The major new feature in 2.0 is the added Python 3.4 support.

New Features

  • Upgraded eGenix PyRun to work with and use Python 2.7.8 per default.

Enhancements / Changes

  • Fixed a bug in the license printer to show the correct license URL.

install-pyrun Quick Install Enhancements

eGenix PyRun includes a shell script called install-pyrun, which greatly simplifies installation of PyRun. It works much like the virtualenv shell script used for creating new virtual environments (except that there's nothing virtual about PyRun environments).

With the script, an eGenix PyRun installation is as simple as running:

./install-pyrun targetdir

This will automatically detect the platform, download and install the right pyrun version into targetdir.

We have updated this script since the last release:

  • Updated install-pyrun to default to eGenix PyRun 2.0.1 and its feature set.

For a complete list of changes, please see the eGenix PyRun Changelog.

Please see the eGenix PyRun 2.0.0 announcement for more details about eGenix PyRun 2.0.

Downloads

Please visit the eGenix PyRun product page for downloads, instructions on installation and documentation of the product.

More Information

For more information on eGenix PyRun, licensing and download instructions, please write to sales@egenix.com.

Enjoy !

Marc-Andre Lemburg, eGenix.com


Django Blog Tutorial - the Next Generation - Part 7

Aug 25 2014 [Archived Version] □ Published at Matthew Daly's Blog

Hello once again! In this instalment we’ll cover:

  • Caching your content with Memcached to improve your site’s performance
  • Refactoring and simplifying our tests
  • Implementing additional feeds
  • Creating a simple search engine

Don’t forget to activate your virtualenv:

source venv/bin/activate

Now let’s get started!

Memcached

If you frequent (or used to frequent) social media sites like Reddit, Slashdot or Digg, you may be familiar with something called variously the Digg or Slashdot effect, whereby if a page gets submitted to a social media site, and subsequently becomes popular, it can be hit by a huge number of HTTP requests in a very short period of time, slowing it down or even taking the server down completely.

Now, as a general rule of thumb, for most dynamic websites such as blogs, the bottleneck is not the web server or the programming language, but the database. If you have a lot of people hitting the same page over and over again in quick succession, then you’re essentially running the same query over and over again and getting the same result each time, which is expensive in terms of processing power. What you need to be able to do is cache the results of the query in memory for a given period of time so that the number of queries is reduced.

That’s where Memcached comes in. It’s a simple key-value store that allows you to store values in memory for a given period of time so that they can be retrieved without having to query the database. Memcached is a very common choice for caching, and is by far the fastest and most efficient type of cache available for Django. It’s also available on Heroku’s free tier.

Django has a very powerful caching framework that supports numerous types of cache in addition to Memcached, such as:

  • Database caching
  • Filesystem caching
  • Local memory caching

There are also third-party backends for using other caching systems such as Redis.

Now, the cache can be used in a number of different ways. You can cache only certain parts of your site if you wish. However, because our site is heavily content-driven, we should be pretty safe to use the per-site cache, which is the simplest way to set up caching.

In order to set up Memcached, there’s a couple of Python libraries we’ll need. If you want to install them locally, however, you’ll need to install both memcached and libmemcached (on Ubuntu, the packages you need are called memcached and libmemcached-dev) on your development machine. If you don’t want to do this, then just copy and paste these lines into requirements.txt instead:

django-pylibmc-sasl==0.2.4
pylibmc==1.3.0

If you are happy to install these dependencies locally, then run this command once memcached and libmemcached are installed:

pip install pylibmc django-pylibmc-sasl

With that done let’s configure Memcached. Open up the settings file and add the following at the bottom:

def get_cache():
  import os
  try:
    os.environ['MEMCACHE_SERVERS'] = os.environ['MEMCACHIER_SERVERS'].replace(',', ';')
    os.environ['MEMCACHE_USERNAME'] = os.environ['MEMCACHIER_USERNAME']
    os.environ['MEMCACHE_PASSWORD'] = os.environ['MEMCACHIER_PASSWORD']
    return {
      'default': {
        'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
        'TIMEOUT': 300,
        'BINARY': True,
        'OPTIONS': { 'tcp_nodelay': True }
      }
    }
  except:
    return {
      'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
      }
    }

CACHES = get_cache()
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = ''

Then add the following to MIDDLEWARE_CLASSES:

    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',

That’s it! The first section configures the application to use Memcached to cache the content when running on Heroku, and sets some configuration parameters, while the second section tells Django to use the per-site cache in order to cache all the site content.

Now, Heroku doesn’t include Memcached by default - instead it’s available as an add-on called Memcachier. To use add-ons you need to set up a credit card for billing. We will set it up to use the free developer plan, but if you outgrow this you can easily switch to a paid plan. To add Memcachier, run this command:

heroku addons:add memcachier:dev

Please note that Memcachier can take a few minutes to get set up, so you may want to leave it a little while between adding it and pushing up your changes. Now we’ll commit our changes:

git add requirements.txt django_tutorial_blog_ng/settings.py
git commit -m 'Implemented caching with Memcached'

Then we’ll push them up to our remote repository and to Heroku:

git push origin master
git push heroku master

And that’s all you need to do to set up Memcached. In addition to storing your query results in Memcached, enabling the caching framework in Django will also set various HTTP headers to enable web proxies and browsers to cache content for an appropriate length of time. If you open up your browser’s developer tools and compare the response headers for your homepage on the latest version of the code with the previous version, you’ll notice that a number of additional headers appear, including Cache-Control, Expires and Last-Modified. These tell web browsers and web proxies how often to request the latest version of the HTML document, in order to help you reduce the bandwidth used.

As you can see, for a site like this where you are the only person adding content, it’s really easy to implement caching with Django, and for a blog there’s very little reason not to do it. If you’re not using Heroku and are instead hosting your site on a VPS, then the configuration will be somewhat different - see here for details. You can also find information on using other cache backends on the same page.

That isn’t all you can do to speed up your site. Heroku doesn’t seem to be very good for serving static files, and if your site is attracting a lot of traffic you might want to host your static files elsewhere, such as on Amazon’s S3 service. Doing so is outside the scope of this tutorial, but for that use case, you should check out django-storages.

Clearing the cache automatically

There is one issue with this implementation. As it is right now, if you view the home page, add a post, then reload the page, you may not see the new post immediately because the cache will continue serving the old version until it has expired. That behaviour is less than ideal - we would like the cache to be cleared automatically when a new post gets added so that users will see the new version immediately. That response will still be cached afterwards, so it only means one extra query.

This is the ideal place to introduce signals. Signals are a way to carry out a given action when an event takes place. In our case, we plan to clear the cache when a post is saved (either created or updated).

Note that as we’ll be testing the behaviour of the cache at this point, you’ll need to install Memcached on your local machine, and we’ll need to change the settings to fall back to our local Memcached instance:

def get_cache():
  import os
  try:
    os.environ['MEMCACHE_SERVERS'] = os.environ['MEMCACHIER_SERVERS'].replace(',', ';')
    os.environ['MEMCACHE_USERNAME'] = os.environ['MEMCACHIER_USERNAME']
    os.environ['MEMCACHE_PASSWORD'] = os.environ['MEMCACHIER_PASSWORD']
    return {
      'default': {
        'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
        'TIMEOUT': 300,
        'BINARY': True,
        'OPTIONS': { 'tcp_nodelay': True }
      }
    }
  except:
    return {
      'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
        'LOCATION': '127.0.0.1:11211'
      }
    }

CACHES = get_cache()
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = ''

If you don’t want to install Memcached locally, you can skip this step, but be aware that the test we write for clearing the cache will always pass if you do skip it.

Then we’ll run our tests to make sure nothing has been broken:

$ python manage.py jenkins blogengine
Creating test database for alias 'default'...
.......................
----------------------------------------------------------------------
Ran 23 tests in 6.164s

OK

Let’s commit:

git add django_tutorial_blog_ng/settings.py
git commit -m 'Now use Memcached in development'

Now we’ll add a test for clearing the cache to PostViewTest:

    def test_clear_cache(self):
        # Create the category
        category = Category()
        category.name = 'python'
        category.description = 'The Python programming language'
        category.save()

        # Create the tag
        tag = Tag()
        tag.name = 'perl'
        tag.description = 'The Perl programming language'
        tag.save()

        # Create the author
        author = User.objects.create_user('testuser', 'user@example.com', 'password')
        author.save()

        # Create the site
        site = Site()
        site.name = 'example.com'
        site.domain = 'example.com'
        site.save()

        # Create the first post
        post = Post()
        post.title = 'My first post'
        post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
        post.slug = 'my-first-post'
        post.pub_date = timezone.now()
        post.author = author
        post.site = site
        post.category = category
        post.save()
        post.tags.add(tag)

        # Check new post saved
        all_posts = Post.objects.all()
        self.assertEquals(len(all_posts), 1)

        # Fetch the index
        response = self.client.get('/')
        self.assertEquals(response.status_code, 200)

        # Create the second post
        post = Post()
        post.title = 'My second post'
        post.text = 'This is [my second blog post](http://127.0.0.1:8000/)'
        post.slug = 'my-second-post'
        post.pub_date = timezone.now()
        post.author = author
        post.site = site
        post.category = category
        post.save()
        post.tags.add(tag)

        # Fetch the index again
        response = self.client.get('/')

        # Check second post present
        self.assertTrue('my second blog post' in response.content)

This should be fairly self-explanatory. We create one post, and request the index page. We then add a second post, request the index page again, and check for the second post. The test should fail because the cached version is returned, rather than the version in the database.

Now we have a test in place, we can implement a fix. First, add this to the top of your models.py:

from django.db.models.signals import post_save
from django.core.cache import cache

Then add the following at the bottom of the file:


# Define signals
def new_post(sender, instance, created, **kwargs):
    cache.clear()

# Set up signals
post_save.connect(new_post, sender=Post)

This is fairly straightforward. What we’re doing is first defining a function called new_post that is called when a new post is created. We then connect it to the post_save signal. When a post is saved, it calls new_post, which clears the cache, making sure users are seeing the latest and greatest version of your site immediately.

Let’s test it:

$ python manage.py jenkins blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 8.473s

OK
Destroying test database for alias 'default'...

There are a number of signals available, and when you create one, you have access to the created object via the instance parameter. Using signals you can implement all kinds of functionality. For instance, you could implement the functionality to send an email when a new post is published.

If you’re using Travis CI, you’ll also need to update the config file:

language: python
python:
- "2.7"
services: memcached
before_install:
    - sudo apt-get install -y libmemcached-dev
# command to install dependencies
install: "pip install -r requirements.txt"
# command to run tests
script: coverage run --include="blogengine/*" --omit="blogengine/migrations/*" manage.py test blogengine
after_success:
    coveralls

Time to commit:

git add blogengine/ .travis.yml
git commit -m 'Now clear cache when post added'

Formatting for RSS feeds

Now, we want to offer more than one option for RSS feeds. For instance, if your blog is aggregated on a site such as Planet Python, but you also blog about JavaScript, you may want to be able to provide a feed for posts in the python category only.

If you have written any posts that use any of Markdown’s custom formatting, you may notice that if you load your RSS feed in a reader, it isn’t formatted as Markdown. Let’s fix that. First we’ll amend our test:

class FeedTest(BaseAcceptanceTest):
    def test_all_post_feed(self):
        # Create the category
        category = Category()
        category.name = 'python'
        category.description = 'The Python programming language'
        category.save()

        # Create the tag
        tag = Tag()
        tag.name = 'python'
        tag.description = 'The Python programming language'
        tag.save()

        # Create the author
        author = User.objects.create_user('testuser', 'user@example.com', 'password')
        author.save()

        # Create the site
        site = Site()
        site.name = 'example.com'
        site.domain = 'example.com'
        site.save()

        # Create a post
        post = Post()
        post.title = 'My first post'
        post.text = 'This is my *first* blog post'
        post.slug = 'my-first-post'
        post.pub_date = timezone.now()
        post.author = author
        post.site = site
        post.category = category

        # Save it
        post.save()

        # Add the tag
        post.tags.add(tag)
        post.save()

        # Check we can find it
        all_posts = Post.objects.all()
        self.assertEquals(len(all_posts), 1)
        only_post = all_posts[0]
        self.assertEquals(only_post, post)

        # Fetch the feed
        response = self.client.get('/feeds/posts/')
        self.assertEquals(response.status_code, 200)

        # Parse the feed
        feed = feedparser.parse(response.content)

        # Check length
        self.assertEquals(len(feed.entries), 1)

        # Check post retrieved is the correct one
        feed_post = feed.entries[0]
        self.assertEquals(feed_post.title, post.title)
        self.assertTrue('This is my <em>first</em> blog post' in feed_post.description)

Don’t forget to run the tests to make sure they fail. Now, let’s fix it:

from django.shortcuts import render
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
from django.contrib.syndication.views import Feed
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
import markdown2

# Create your views here.
class CategoryListView(ListView):
    def get_queryset(self):
        slug = self.kwargs['slug']
        try:
            category = Category.objects.get(slug=slug)
            return Post.objects.filter(category=category)
        except Category.DoesNotExist:
            return Post.objects.none()


class TagListView(ListView):
    def get_queryset(self):
        slug = self.kwargs['slug']
        try:
            tag = Tag.objects.get(slug=slug)
            return tag.post_set.all()
        except Tag.DoesNotExist:
            return Post.objects.none()


class PostsFeed(Feed):
    title = "RSS feed - posts"
    link = "feeds/posts/"
    description = "RSS feed - blog posts"

    def items(self):
        return Post.objects.order_by('-pub_date')

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        extras = ["fenced-code-blocks"]
        content = mark_safe(markdown2.markdown(force_unicode(item.text),
                                               extras = extras))
        return content

All we’re doing here is amending the item_description method of PostsFeed to render it as Markdown. Now let’s run our tests again:

$ python manage.py jenkins
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 9.370s

OK
Destroying test database for alias 'default'...

With that done, we’ll commit our changes:

git add blogengine/
git commit -m 'Fixed rendering for post feed'

Refactoring our tests

Now, before we get into implementing the feed, our tests are a bit verbose. We create a lot of items over and over again - let’s sort that out. Factory Boy is a handy Python module that allows you to create easy-to-use factories for creating objects over and over again in tests. Let’s install it:

pip install factory_boy
pip freeze > requirements.txt
git add requirements.txt
git commit -m 'Installed Factory Boy'

Now let’s set up a factory for creating posts. Add this at the top of the test file:

import factory.django

Then, before your actual tests, insert the following:

# Factories
class SiteFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Site
        django_get_or_create = (
            'name',
            'domain'
        )

    name = 'example.com'
    domain = 'example.com'

Now, wherever you call Site(), add its attributes, and save it, replace those lines with the following:

        site = SiteFactory()

Much simpler and more concise, I’m sure you’ll agree! Now, let’s run the tests to make sure they aren’t broken:

$ python manage.py test blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 7.482s

OK
Destroying test database for alias 'default'...

Let’s commit again: bash git add blogengine/tests.py git commit -m 'Now use Factory Boy for site objects'

Let’s do the same thing with Category objects:

class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Category
        django_get_or_create = (
            'name',
            'description',
            'slug'
        )

    name = 'python'
    description = 'The Python programming language'
    slug = 'python'

Again, just find every time we call Category() and replace it with the following:

        category = CategoryFactory()

Now if we run our tests, we’ll notice a serious error:

$ python manage.py test blogengine
Creating test database for alias 'default'...
EE..EE.EE.EE...E.EEE..E.
======================================================================
ERROR: test_create_category (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 42, in test_create_category
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_create_post (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 80, in test_create_post
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 339, in test_create_post
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_create_post_without_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 378, in test_create_post_without_tag
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_delete_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 245, in test_delete_category
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_delete_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 467, in test_delete_post
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_edit_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 221, in test_edit_category
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 410, in test_edit_post
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_all_post_feed (blogengine.tests.FeedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 810, in test_all_post_feed
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_category_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 640, in test_category_page
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_clear_cache (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 753, in test_clear_cache
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_index (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 518, in test_index
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create


    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

======================================================================
ERROR: test_post_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 576, in test_post_page
    category = CategoryFactory()
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
    return cls.create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
    return cls._generate(True, attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
    obj = cls._prepare(create, **attrs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
    return cls._create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create


    return cls._get_or_create(model_class, *args, **kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
    obj, _created = manager.get_or_create(*args, **key_fields)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
    return self.get_queryset().get_or_create(**kwargs)
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
    obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'

----------------------------------------------------------------------
Ran 24 tests in 5.162s

FAILED (errors=13)
Destroying test database for alias 'default'...

Thankfully, this is easy to fix. We just need to amend the custom save() method of the Category model:

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(unicode(self.name))
        super(Category, self).save(*args, **kwargs)

That should resolve the issue:

$ python manage.py jenkins
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 7.749s

OK
Destroying test database for alias 'default'...

Let’s commit again:

git add blogengine/
git commit -m 'Category now uses Factory Boy'

Now let’s do the same thing for tags:

class TagFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Tag
        django_get_or_create = (
            'name',
            'description',
            'slug'
        )

    name = 'python'
    description = 'The Python programming language'
    slug = 'python'

And replace the sections where we create new Tag objects:

        tag = TagFactory()

Note that some tags have different values. We can easily pass different values to our TagFactory() to override the default values:

        tag = TagFactory(name='perl', description='The Perl programming language')

The Tag model has the same issue as the Category one did, so let’s fix that:

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(unicode(self.name))
        super(Tag, self).save(*args, **kwargs)

We run our tests again:

$ python manage.py test blogengine/
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 7.153s

OK
Destroying test database for alias 'default'...

Time to commit again:

git add blogengine/
git commit -m 'Now use Factory Boy for tags'

Next we’ll create a factory for adding users. Note that the factory name doesn’t have to match the object name, so you can create factories for different types of users. Here we create a factory for authors - you could, for instance, create a separate factory for subscribers if you wanted:

class AuthorFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
        django_get_or_create = ('username','email', 'password',)

    username = 'testuser'
    email = 'user@example.com'
    password = 'password'

And as before, replace those sections where we create users with the following:

        author = AuthorFactory()

Run the tests again:

$ python manage.py test blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 5.808s

OK
Destroying test database for alias 'default'...

We commit our changes:

git add blogengine/
git commit -m 'Now use Factory Boy for creating authors'

Now we’ll create a flat page factory:



class FlatPageFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = FlatPage
        django_get_or_create = (
            'url',
            'title',
            'content'
        )

    url = '/about/'
    title = 'About me'
    content = 'All about me'

And use it for our flat page test: python blogengine/tests.py page = FlatPageFactory()

Check the tests pass:

$ python manage.py test blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 5.796s

OK
Destroying test database for alias 'default'...

And commit again:

git add blogengine/
git commit -m 'Now use Factory Boy for flat page test'

Now we’ll create a final factory for posts:

class PostFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Post
        django_get_or_create = (
            'title',
            'text',
            'slug',
            'pub_date'
        )

    title = 'My first post'
    text = 'This is my first blog post'
    slug = 'my-first-post'
    pub_date = timezone.now()
    author = factory.SubFactory(AuthorFactory)
    site = factory.SubFactory(SiteFactory)
    category = factory.SubFactory(CategoryFactory)

This factory is a little bit different. Because our Post model depends on several others, we need to be able to create those additional objects on demand. By designating them as subfactories, we can easily create the associated objects for our Post object.

That means that not only can we get rid of our Post() calls, but we can also get rid of the factory calls to create the associated objects for Post models. Again, I’ll leave actually doing this as an exercise for the reader, but you can always refer to the GitHub repository if you’re not too sure.

Make sure your tests still pass, then commit the changes:

git add blogengine/
git commit -m 'Now use Factory Boy for testing posts'

Using Factory Boy made a big difference to the size of the test file - I was able to cut it down by over 200 lines of code. As your application gets bigger, it gets harder to maintain, so do what you can to keep the size down.

Additional RSS feeds

Now, let’s implement our additional RSS feeds. First, we’ll write a test for the category feed. Add this to the FeedTest class:

    def test_category_feed(self):
        # Create a post
        post = PostFactory(text='This is my *first* blog post')

        # Create another post in a different category
        category = CategoryFactory(name='perl', description='The Perl programming language', slug='perl')
        post2 = PostFactory(text='This is my *second* blog post', title='My second post', slug='my-second-post', category=category)

        # Fetch the feed
        response = self.client.get('/feeds/posts/category/python/')
        self.assertEquals(response.status_code, 200)

        # Parse the feed
        feed = feedparser.parse(response.content)

        # Check length
        self.assertEquals(len(feed.entries), 1)

        # Check post retrieved is the correct one
        feed_post = feed.entries[0]
        self.assertEquals(feed_post.title, post.title)
        self.assertTrue('This is my <em>first</em> blog post' in feed_post.description)

        # Check other post is not in this feed
        self.assertTrue('This is my <em>second</em> blog post' not in response.content)

Here we create two posts in different categories (note that we create a new category and override the post category for it). We then fetch `/feeds/posts/category/python/‘ and assert that it contains only one post, with the content of the first post and not the content of the second.

Run the tests and they should fail:

$ python manage.py test blogengine
Creating test database for alias 'default'...
................F........
======================================================================
FAIL: test_category_feed (blogengine.tests.FeedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 716, in test_category_feed
    self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200

----------------------------------------------------------------------
Ran 25 tests in 5.804s

FAILED (failures=1)
Destroying test database for alias 'default'...

Because we haven’t yet implemented that route, we get a 404 error. So let’s create a route for this:

from django.conf.urls import patterns, url
from django.views.generic import ListView, DetailView
from blogengine.models import Post, Category, Tag
from blogengine.views import CategoryListView, TagListView, PostsFeed, CategoryPostsFeed

urlpatterns = patterns('',
    # Index
    url(r'^(?P<page>\d+)?/?$', ListView.as_view(
        model=Post,
        paginate_by=5,
        )),

    # Individual posts
    url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
        model=Post,
        )),

    # Categories
    url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
        paginate_by=5,
        model=Category,
        )),

    # Tags
    url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
        paginate_by=5,
        model=Tag,
        )),

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

    # Category RSS feed
    url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),
)

Note that the category RSS feed route is similar to the post RSS feed route, but accepts a slug parameter. We will use this to pass through the slug for the category in question. Also note we import the CategoryPostsFeed view. Now, we need to create that view. Fortunately, because it’s written as a Python class, we can extend the existing PostsFeed class. Open up your views file and amend it to look like this:

from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
from django.contrib.syndication.views import Feed
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
import markdown2

# Create your views here.
class CategoryListView(ListView):
    def get_queryset(self):
        slug = self.kwargs['slug']
        try:
            category = Category.objects.get(slug=slug)
            return Post.objects.filter(category=category)
        except Category.DoesNotExist:
            return Post.objects.none()


class TagListView(ListView):
    def get_queryset(self):
        slug = self.kwargs['slug']
        try:
            tag = Tag.objects.get(slug=slug)
            return tag.post_set.all()
        except Tag.DoesNotExist:
            return Post.objects.none()


class PostsFeed(Feed):
    title = "RSS feed - posts"
    description = "RSS feed - blog posts"
    link = '/'

    def items(self):
        return Post.objects.order_by('-pub_date')

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        extras = ["fenced-code-blocks"]
        content = mark_safe(markdown2.markdown(force_unicode(item.text),
                                               extras = extras))
        return content


class CategoryPostsFeed(PostsFeed):
    def get_object(self, request, slug):
        return get_object_or_404(Category, slug=slug)

    def title(self, obj):
        return "RSS feed - blog posts in category %s" % obj.name

    def link(self, obj):
        return obj.get_absolute_url()

    def description(self, obj):
        return "RSS feed - blog posts in category %s" % obj.name

    def items(self, obj):
        return Post.objects.filter(category=obj).order_by('-pub_date')

Note that many of our fields don’t have to be explicitly defined as they are inherited from PostsFeed. We can’t hard-code the title, link or description because they depend on the category, so we instead define methods to return the appropriate text.

Also note get_object() - we define this so that we can ensure the category exists. If it doesn’t exist, then it returns a 404 error rather than showing an empty feed.

We also override items() to filter it to just those posts that are in the given category.

If you run the tests again, they should now pass:

$ python manage.py test blogengine
Creating test database for alias 'default'...
.........................
----------------------------------------------------------------------
Ran 25 tests in 5.867s

OK
Destroying test database for alias 'default'...

Let’s commit our changes:

git add blogengine/
git commit -m 'Implemented category RSS feed'

Now, we can get our category RSS feed, but how do we navigate to it? Let’s add a link to each category page that directs a user to its RSS feed. To do so, we’ll need to create a new template for category pages. First, let’s add some code to our tests to ensure that the right template is used at all times. Add the following to the end of the test_index method of PostViewTest:

        # Check the correct template was used
        self.assertTemplateUsed(response, 'blogengine/post_list.html')

Then, add this to the end of test_post_page:


        # Check the correct template was used
        self.assertTemplateUsed(response, 'blogengine/post_detail.html')

Finally, add this to the end of test_category_page:

        # Check the correct template was used
        self.assertTemplateUsed(response, 'blogengine/category_post_list.html')

These assertions confirm which template was used to generate which request.

Next, we head into our views file:

class CategoryListView(ListView):
    template_name = 'blogengine/category_post_list.html'

    def get_queryset(self):
        slug = self.kwargs['slug']
        try:
            category = Category.objects.get(slug=slug)
            return Post.objects.filter(category=category)
        except Category.DoesNotExist:
            return Post.objects.none()

    def get_context_data(self, **kwargs):
        context = super(CategoryListView, self).get_context_data(**kwargs)
        slug = self.kwargs['slug']
        try:
            context['category'] = Category.objects.get(slug=slug)
        except Category.DoesNotExist:
            context['category'] = None
        return context

Note that we first of all change the template used by this view. Then, we override get_context_data to add in additional data. What we’re doing is getting the slug that was passed through, looking up any category for which it is the slug, and returning it as additional context data. Using this method, you can easily add additional data that you may wish to render in your Django templates.

Finally, we create our new template:

{% extends "blogengine/includes/base.html" %}

    {% load custom_markdown %}

    {% block content %}
        {% if object_list %}
            {% for post in object_list %}
            <div class="post col-md-12">
            <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
            <h3>{{ post.pub_date }}</h3>
            {{ post.text|custom_markdown }}
            </div>
            {% if post.category %}
            <div class="col-md-12">
            <a href="{{ post.category.get_absolute_url }}"><span class="label label-primary">{{ post.category.name }}</span></a>
            </div>
            {% endif %}
            {% if post.tags %}
            <div class="col-md-12">
            {% for tag in post.tags.all %}
            <a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
            {% endfor %}
            </div>
            {% endif %}
            {% endfor %}
        {% else %}
            <p>No posts found</p>
        {% endif %}

        <ul class="pager">
        {% if page_obj.has_previous %}
        <li class="previous"><a href="http://matthewdaly.co.uk/{{ page_obj.previous_page_number }}/">Previous Page</a></li>
        {% endif %}
        {% if page_obj.has_next %}
        <li class="next"><a href="http://matthewdaly.co.uk/{{ page_obj.next_page_number }}/">Next Page</a></li>
        {% endif %}
        </ul>

        <a href="http://matthewdaly.co.uk/feeds/posts/category/{{ category.slug }}/">RSS feed for category {{ category.name }}</a>

    {% endblock %}

Note that the category has been passed through to the template and is now accessible. If you run the tests, they should now pass:

$ python manage.py jenkins
Creating test database for alias 'default'...
.........................
----------------------------------------------------------------------
Ran 25 tests in 7.232s

OK
Destroying test database for alias 'default'...

With that done. we can commit our changes:

git add templates/ blogengine/
git commit -m 'Added link to RSS feed from category page'

Next up, let’s implement another RSS feed for tags. First, we’ll implement our test:

      def test_tag_feed(self):
          # Create a post
          post = PostFactory(text='This is my *first* blog post')
          tag = TagFactory()
          post.tags.add(tag)
          post.save()

          # Create another post with a different tag
          tag2 = TagFactory(name='perl', description='The Perl programming language', slug='perl')
          post2 = PostFactory(text='This is my *second* blog post', title='My second post', slug='my-second-post')
          post2.tags.add(tag2)
          post2.save()

          # Fetch the feed
          response = self.client.get('/feeds/posts/tag/python/')
          self.assertEquals(response.status_code, 200)

          # Parse the feed
          feed = feedparser.parse(response.content)

          # Check length
          self.assertEquals(len(feed.entries), 1)

          # Check post retrieved is the correct one
          feed_post = feed.entries[0]
          self.assertEquals(feed_post.title, post.title)
          self.assertTrue('This is my <em>first</em> blog post' in feed_post.description)

          # Check other post is not in this feed
          self.assertTrue('This is my <em>second</em> blog post' not in response.content)

This is virtually identical to the test for the categroy feed, but we adjust it to work with the Tag attribute and change the URL. Let’s check that our test fails:

$ python manage.py test blogengine
Creating test database for alias 'default'...
.................F........
======================================================================
FAIL: test_tag_feed (blogengine.tests.FeedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 757, in test_tag_feed
    self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200

----------------------------------------------------------------------
Ran 26 tests in 5.760s

FAILED (failures=1)
Destroying test database for alias 'default'...

As before, we create a route for this:

from django.conf.urls import patterns, url
from django.views.generic import ListView, DetailView
from blogengine.models import Post, Category, Tag
from blogengine.views import CategoryListView, TagListView, PostsFeed, CategoryPostsFeed, TagPostsFeed

urlpatterns = patterns('',
    # Index
    url(r'^(?P<page>\d+)?/?$', ListView.as_view(
        model=Post,
        paginate_by=5,
        )),

    # Individual posts
    url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
        model=Post,
        )),

    # Categories
    url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
        paginate_by=5,
        model=Category,
        )),

    # Tags
    url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
        paginate_by=5,
        model=Tag,
        )),

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

    # Category RSS feed
    url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),

    # Tag RSS feed
    url(r'^feeds/posts/tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagPostsFeed()),
)

Next, we create our view:

class TagPostsFeed(PostsFeed):
    def get_object(self, request, slug):
        return get_object_or_404(Tag, slug=slug)

    def title(self, obj):
        return "RSS feed - blog posts tagged  %s" % obj.name

    def link(self, obj):
        return obj.get_absolute_url()

    def description(self, obj):
        return "RSS feed - blog posts tagged %s" % obj.name

    def items(self, obj):
        try:
            tag = Tag.objects.get(slug=obj.slug)
            return tag.post_set.all()
        except Tag.DoesNotExist:
            return Post.objects.none()

Again, this inherits from PostsFeed, but the syntax for getting posts matching a tag is slightly different because they use a many-to-many relationship.

We also need a template for the tag pages. Add this to the end of the test_tag_page method:


        # Check the correct template was used
        self.assertTemplateUsed(response, 'blogengine/tag_post_list.html')

Let’s create that template:

{% extends "blogengine/includes/base.html" %}

    {% load custom_markdown %}

    {% block content %}
        {% if object_list %}
            {% for post in object_list %}
            <div class="post col-md-12">
            <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
            <h3>{{ post.pub_date }}</h3>
            {{ post.text|custom_markdown }}
            </div>
            {% if post.category %}
            <div class="col-md-12">
            <a href="{{ post.category.get_absolute_url }}"><span class="label label-primary">{{ post.category.name }}</span></a>
            </div>
            {% endif %}
            {% if post.tags %}
            <div class="col-md-12">
            {% for tag in post.tags.all %}
            <a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
            {% endfor %}
            </div>
            {% endif %}
            {% endfor %}
        {% else %}
            <p>No posts found</p>
        {% endif %}

        <ul class="pager">
        {% if page_obj.has_previous %}
        <li class="previous"><a href="http://matthewdaly.co.uk/{{ page_obj.previous_page_number }}/">Previous Page</a></li>
        {% endif %}
        {% if page_obj.has_next %}
        <li class="next"><a href="http://matthewdaly.co.uk/{{ page_obj.next_page_number }}/">Next Page</a></li>
        {% endif %}
        </ul>

        <a href="http://matthewdaly.co.uk/feeds/posts/tag/{{ tag.slug }}/">RSS feed for tag {{ tag.name }}</a>

    {% endblock %}

This is virtually identical to the category template. You’ll also need to apply this template in the view for the tag list, and pass the tag name through as context data:

class TagListView(ListView):
    template_name = 'blogengine/tag_post_list.html'

    def get_queryset(self):
        slug = self.kwargs['slug']
        try:
            tag = Tag.objects.get(slug=slug)
            return tag.post_set.all()
        except Tag.DoesNotExist:
            return Post.objects.none()

    def get_context_data(self, **kwargs):
        context = super(TagListView, self).get_context_data(**kwargs)
        slug = self.kwargs['slug']
        try:
            context['tag'] = Tag.objects.get(slug=slug)
        except Tag.DoesNotExist:
            context['tag'] = None
        return context

Let’s run our tests:

$ python manage.py test blogengine
Creating test database for alias 'default'...
..........................
----------------------------------------------------------------------
Ran 26 tests in 5.770s

OK
Destroying test database for alias 'default'...

You may want to do a quick check to ensure your tag feed link works as expected. Time to commit:

git add blogengine templates
git commit -m 'Implemented tag feeds'

Moving our templates

Before we crack on with implementing search, there’s one more piece of housekeeping. In Django, templates can be applied at project level or at app level. So far, we’ve been storing them in the project, but we would like our app to be as self-contained as possible so it can just be dropped into future projects where we need a blog. That way, it can be easily overridden for specific projects. You can move the folders and update the Git repository at the same time with this command:

git mv templates/ blogengine/

We run the tests to make sure nothing untoward has happened:

$ python manage.py test
Creating test database for alias 'default'...
..........................
----------------------------------------------------------------------
Ran 26 tests in 5.847s

OK
Destroying test database for alias 'default'...

And we commit:

git commit -m 'Moved templates'

Note that git mv updates Git and moves the files, so you don’t need to call git add.

Implementing search

For our final task today, we will be implementing a very simple search engine. Our requirements are:

  • It should be in the header, to allow for easy access from anywhere in the front end.
  • It should search the title and text of posts.

First, we’ll write our tests:

class SearchViewTest(BaseAcceptanceTest):
    def test_search(self):
        # Create a post
        post = PostFactory()

        # Create another post
        post2 = PostFactory(text='This is my *second* blog post', title='My second post', slug='my-second-post')

        # Search for first post
        response = self.client.get('/search?q=first')
        self.assertEquals(response.status_code, 200)

        # Check the first post is contained in the results
        self.assertTrue('My first post' in response.content)

        # Check the second post is not contained in the results
        self.assertTrue('My second post' not in response.content)

        # Search for second post
        response = self.client.get('/search?q=second')
        self.assertEquals(response.status_code, 200)

        # Check the first post is not contained in the results
        self.assertTrue('My first post' not in response.content)

        # Check the second post is contained in the results
        self.assertTrue('My second post' in response.content)

Don’t forget to run the tests to make sure they fail:

 python manage.py test
Creating test database for alias 'default'...
..........................F
======================================================================
FAIL: test_search (blogengine.tests.SearchViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 819, in test_search
    self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200

----------------------------------------------------------------------
Ran 27 tests in 6.919s

FAILED (failures=1)
Destroying test database for alias 'default'...

With that done, we can add the search form to the header:

“` html blogengine/templates/blogengine/includes/base.html <!DOCTYPE html>

{% block title %}My Django Blog{% endblock %}

    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->

    {% load staticfiles %}
    <link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/normalize.css' %}">
    <link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/main.css' %}">
    <link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap-theme.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    <link rel="stylesheet" href="{% static 'css/code.css' %}">
    <script src="{% static 'bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js' %}"></script>
</head>
<body>
    <!--[if lt IE 7]>
        <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
    <![endif]-->

    <!-- Add your site or application content here -->

    <div id="fb-root"></div>
    <script>(function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) return;
            js = d.createElement(s); js.id = id;
            js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));</script>

    <div class="navbar navbar-static-top navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="http://matthewdaly.co.uk/">My Django Blog</a>
            </div>
            <div class="collapse navbar-collapse" id="header-nav">
                <ul class="nav navbar-nav">
                    {% load flatpages %}
                    {% get_flatpages as flatpages %}
                    {% for flatpage in flatpages %}
                    <li><a href="{{ flatpage.url }}">{{ flatpage.title }}</a></li>
                    {% endfor %}
                    <li><a href="http://matthewdaly.co.uk/feeds/posts/">RSS feed</a></li>

                    <form action="/search" method="GET" class="navbar-form navbar-left">
                        <div class="form-group">
                            <input type="text" name="q" placeholder="Search..." class="form-control"></input>
                        </div>
                        <button type="submit" class="btn btn-default">Search</button>
                    </form>
                </ul>
            </div>
        </div>
    </div>

    <div class="container">
        {% block header %}
            <div class="page-header">
                <h1>My Django Blog</h1>
            </div>
        {% endblock %}

        <div class="row">
            {% block content %}{% endblock %}
        </div>
    </div>

    <div class="container footer">
        <div class="row">
            <div class="span12">
                <p>Copyright &copy; {% now "Y" %}</p>
            </div>
        </div>
    </div>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
    <script>window.jQuery || document.write('<script src="{% static 'bower_components/html5-boilerplate/js/vendor/jquery-1.10.2.min.js' %}"><\/script>')</script>
    <script src="{% static 'bower_components/html5-boilerplate/js/plugins.js' %}"></script>
    <script src="{% static 'bower_components/bootstrap/dist/js/bootstrap.min.js' %}"></script>

    <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
    <script>
        (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
        function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
        e=o.createElement(i);r=o.getElementsByTagName(i)[0];
        e.src='//www.google-analytics.com/analytics.js';
        r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
        ga('create','UA-XXXXX-X');ga('send','pageview');
    </script>
</body>

“`

Now we’ll actually implement our search. Implementing search using Django’s generic views can be fiddly, so we’ll write our search view as a function instead. First, amend the imports at the top of your view file to look like this:

from django.shortcuts import get_object_or_404, render_to_response
from django.core.paginator import Paginator, EmptyPage
from django.db.models import Q
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
from django.contrib.syndication.views import Feed
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
import markdown2

Next, add the following code to the end of the file:

def getSearchResults(request):
    """
    Search for a post by title or text
    """
    # Get the query data
    query = request.GET.get('q', '')
    page = request.GET.get('page', 1)

    # Query the database
    if query:
        results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
    else:
        results = None

    # Add pagination
    pages = Paginator(results, 5)

    # Get specified page
    try:
        returned_page = pages.page(page)
    except EmptyPage:
        returned_page = pages.page(pages.num_pages)

    # Display the search results
    return render_to_response('blogengine/search_post_list.html',
                              {'page_obj': returned_page,
                               'object_list': returned_page.object_list,
                               'search': query})

As this is the first time we’ve written a view without using generic views, a little explanation is called for. First we get the values of the q and page parameters passed to the view. q contains the query text and page contains the page number. Note also that our page defaults to 1 if not set.

We then use the Q object to perform a query. The Django ORM will AND together keyword argument queries, but that’s not the behaviour we want here. Instead we want to be able to search for content in the title or text, so we need to use a query with an OR statement, which necessitates using the Q object.

Next, we use the Paginator object to manually paginate the results, and if it raises an EmptyPage exception, to just show the last page instead. Finally we render the template blogengine/search_post_list.html, and pass through the parameters page_obj for the returned page, object_list for the objects, and search for the query.

We also need to add a route for our new view:


    # Search posts
    url(r'^search', 'blogengine.views.getSearchResults'),

Finally, let’s create a new template to show our results:

{% extends "blogengine/includes/base.html" %}

    {% load custom_markdown %}

    {% block content %}
        {% if object_list %}
            {% for post in object_list %}
            <div class="post col-md-12">
            <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
            <h3>{{ post.pub_date }}</h3>
            {{ post.text|custom_markdown }}
            </div>
            {% if post.category %}
            <div class="col-md-12">
            <a href="{{ post.category.get_absolute_url }}"><span class="label label-primary">{{ post.category.name }}</span></a>
            </div>
            {% endif %}
            {% if post.tags %}
            <div class="col-md-12">
            {% for tag in post.tags.all %}
            <a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
            {% endfor %}
            </div>
            {% endif %}
            {% endfor %}
        {% else %}
            <p>No posts found</p>
        {% endif %}

        <ul class="pager">
        {% if page_obj.has_previous %}
        <li class="previous"><a href="http://matthewdaly.co.uk/search?page={{ page_obj.previous_page_number }}&q={{ search }}">Previous Page</a></li>
        {% endif %}
        {% if page_obj.has_next %}
        <li class="next"><a href="http://matthewdaly.co.uk/search?page={{ page_obj.next_page_number }}&q={{ search }}">Next Page</a></li>
        {% endif %}
        </ul>

    {% endblock %}

Let’s run our tests:

$ python manage.py test
Creating test database for alias 'default'...
...........................
----------------------------------------------------------------------
Ran 27 tests in 6.421s

OK
Destroying test database for alias 'default'...

Don’t forget to do a quick sense check to make sure it’s all working as expected. Then it’s time to commit:

git add blogengine/
git commit -m 'Implemented search'

And push up your changes:

git push origin master
git push heroku master

And that’s the end of this instalment. Please note this particular search solution is quite basic, and if you want something more powerful, you may want to look at Haystack.

As usual, you can get this lesson with git checkout lesson-7 - if you have any problems, the repository should be the first place you look for answers as this is the working code base for the application, and with judicious use of a tool like diff, it’s generally pretty easy to track down most issues.

In our next, and final instalment, we’ll cover:

  • Tidying everything up
  • Implementing an XML sitemap for search engines
  • Optimising our site
  • Using Fabric to make deployment easier

Hope to see you then!


Chaussette and Circus as a new way to deploy your Django applications

Aug 24 2014 [Archived Version] □ Published at RkBlog - Python, Linux, Astronomy under tags  django web framework tutorials

Many Python web applications run on production with the help of gunicorn or uwsgi. There are however other solutions that may turn out to be even better than the currently popular solutions. One of alternatives is chaussette and circus. Chaussette is a WSGI server that can serve WSGI applications like your Django project. Circus is an application to control and monitor processes and sockets. It can manage chaussette, celery or other sockets and processes making it a manager of all project services.


Password reset for mobile application with Django and Tastypie

Aug 24 2014 [Archived Version] □ Published at michalcodes4life under tags  django programming python rest tastypie

Django offers ready password reset views for web applications. With a couple of tweaks it can be converted into an API and used in your mobile applications. Form This PasswordResetForm is essentialy a copy of django.contrib.auth.forms.PasswordResetForm with a few changes (outlined in the comments). This code is from Django 1.7, previous versions slightly differ. Email […]


What's New With WhisperGifts

Aug 24 2014 [Archived Version] □ Published at Ross Poulton under tags  django whispergifts

It's been a busy few months for WhisperGifts, my bridal gift registry service built with Django. This post rounds up a few recent additions and changes, including technical details for those who want to do something similar. Highlights include weather forecasting, REST APIs, and JavaScript bookmarklets.


What's New With WhisperGifts

Aug 24 2014 [Archived Version] □ Published at Ross Poulton under tags  django whispergifts

It's been a busy few months for WhisperGifts, my bridal gift registry service built with Django. This post rounds up a few recent additions and changes, including technical details for those who want to do something similar. Highlights include weather forecasting, REST APIs, and JavaScript bookmarklets.


A thank you to the companies that support Django's infrastructure

Aug 23 2014 [Archived Version] □ Published at The Django weblog

It's not surprising that a project developing an open source web framework will, from time to time, require server resources. When a project gets to be the size of Django, the server resources needed to support the project can get expensive.

Luckily, we don't have to cover these costs ourselves. We have a lot of very generous companies in our community that donate server resources to the Django Software Foundation (DSF) so that Django can keep running.

There are two companies in particular that deserve to be pointed out for their generous contributions supporting the Django project's infrastructure.

Firstly, Rackspace donates server instances that we use to run the main djangoproject.com web site, plus docs.djangoproject.com. These sites generate a lot of traffic - millions of hits per month - and they're the most visible parts of Django's public presence. In addition, Rackespace donates servers to run Django's Continuous Integration (CI) servers. These servers run the full Django test suite on every supported database and platform to make sure that we don't ship a release with a known problem on a supported platform.

Secondly, Heroku recently donated $1500 in service credits to the DSF. This matches a similar grant they gave the DSF last year. We use Heroku to host such services as dashboard.djangoproject.com, people.djangoproject.com, djangosnippets.org, and a Sentry instance.

A huge thanks to Rackspace and Heroku for their generous contributions to the Django community!

UPDATE 25 August 7:00 CDT: Another company that we neglected to mention is Divio. Django's CI infrastructure has only recently been moved to Rackspace; for several years prior to this move, Divio provided the hosting for our CI tools. Although Divio is no longer providing this resource, it was invaluable for the several years that we were using it. A belated Thank You to goes to Divio for their generous contribution.


Productivity Tools & Processes

Aug 23 2014 [Archived Version] □ Published at Nerdy Dork under tags  programming &amp; internet

Last week as I was driving to a user group meeting, I listed to a podcast episode. The subject was managing email. Since then I have cleaned up my inbox and I have been at inbox zero or close to it all week. Between my 5 email accounts I have 1 message in my inbox […]

The post Productivity Tools & Processes appeared first on Dustin Davis.


Django REST framework Sprint at DjangoCon US 2014

Aug 21 2014 [Archived Version] □ Published at José Padilla under tags  django djangocon

image

This’ll be my first time attending and speaking at DjangoCon US. I’ll be talking about JSON Web Tokens, Django, and Django REST Framework.

I’ve been talking about working on a Sprint for Django REST Framework with Tom Christie on Twitter. Tom pointed out a list of decent candidates to work on if we get some people together. We’ll have a tagged list of obvious bug candidates before the sprint. He’s also planning on helping us out remotely those days.

It seems like a great idea to celebrate the successful Django REST framework 3 campaign on Kickstarter and with the timing of DjangoCon, “kickstart” the development of upcoming versions.

So, I’m recruiting anyone who’d be interested and available in working together. Everyone’s welcome to join in - if you use Django REST framework and would like to become more familiar with its internals while working together with other developers with various levels of expertise. Sprints will be held Friday and Saturday Sep 5-6. 
If you’re interested please send me an email so we can start getting organized. If you know someone who might be interested please share this with them.


PyDDF Python Sprint 2014

Aug 21 2014 [Archived Version] □ Published at eGenix.com News & Events

The following text is in German, since we're announcing a Python sprint in Düsseldorf, Germany.

Ankündigung

Der erste PyDDF Python Sprint 2014 in Düsseldorf:

Samstag, 27.09.2014, 10:00-18:00 Uhr
Sonntag, 28.09.2014, 10:00-18:00 Uhr
Seminarraum 25.41.00.45
(Gebäude 25.41, Erdgeschoss, Raum 45) des
ZIM der HHU Düsseldorf

Informationen

Das Python Meeting Düsseldorf (PyDDF) veranstaltet zusammen mit dem ZIM der Heinrich-Heine-Universität Düsseldorf ein Python Sprint Wochenende im September.

Der Sprint findet am Wochenende 27/28.09.2013 im Seminarraum 25.41.00.45 (Gebäude 25.41, Erdgeschoss, Raum 45) des ZIM der HHU Düsseldorf stattfinden:
Folgende Themengebiete haben wir als Anregung angedacht:
  • Openpyxl
Openpyxl ist eine Python Bibliothek, mit der man Excel 2010 XLSX/XLSM Dateien lesen und schreiben kann.

Charlie ist Co-Maintainer des Pakets und würde gerne an folgenden Themen arbeiten:

- ElementTree Implementation des lxml.etree.xmlfile Moduls (context manager)
- Co-Routines für die Serialisierung
- Python Code-Object-Generierung anhand des Schemas
  • HTTP Audio Streaming für Mopidy
Mopidy ist ein MPD Musikserver, der viele Internet-Streaming-Dienste abonnieren kann, diese jedoch nur über lokale Audiogeräte ausgibt.

Es wäre schön, wenn man auch Internetradios anschließen könnte, wie z.B. die Squeezebox. Es gibt dazu schon ein Ticket, auf dem man vermutlich aufbauen könnte:

- https://github.com/mopidy/mopidy/issues/56

Ziel wäre es, eine Mopidy Extension zu schreiben, die dieses Feature umsetzt.
Natürlich kann jeder Teilnehmer weitere Themen vorschlagen, z.B.
  • Kivy (Python auf Android/iOS)
  • RaspberryPi (wir werden ein paar davon mitbringen)
  • FritzConnection (Python API für die Fritzbox)
  • OpenCV (Bilder von Webcams mit Python verarbeiten)
  • u.a.
Alles weitere und die Anmeldung findet Ihr auf der Sprint Seite:
Teilnehmer sollten sich zudem auf der PyDDF Liste anmelden, da wir uns dort koordinieren:
Wir haben nur begrenzten Platz im Seminarraum, daher wäre es gut, wenn wir die ungefähre Anzahl Teilnehmer schon in Vorfeld einplanen könnten. Platz ist für max. 30 Teilnehmer.

Über das Python Meeting Düsseldorf

Das Python Meeting Düsseldorf ist eine regelmäßige Veranstaltung in Düsseldorf, die sich an Python Begeisterte aus der Region wendet.

Einen guten Überblick über die Vorträge bietet unser PyDDF YouTube-Kanal, auf dem wir Videos der Vorträge nach den Meetings veröffentlichen.

Veranstaltet wird das Meeting von der eGenix.com GmbH, Langenfeld, in Zusammenarbeit mit Clark Consulting & Research, Düsseldorf.

Viel Spaß !

Marc-Andre Lemburg, eGenix.com

Published: 2013-03-20


django-planet aggregates posts from Django-related blogs. It is not affiliated with or endorsed by the Django Project.

Social Sharing

Feeds

Tag cloud

admin administration advanced ajax apache api app appengine app engine apple aprendiendo python architecture articles asides audrey authentication automation backup bash basics bitbucket blog blog action day book books buildout business cache celery celerycrawler challenges cherokee choices class-based-views cliff cloud code codeship coding command configuration continuous deployment continuous integration couchdb css d data database databases debian deploy deployment developers development digitalocean django djangocon django-readonly-site django-rest-framework django templates documentation dojango dojo dreamhost dughh eclipse education email encoding error events extensions fabric facebook family fashiolista fedora field filter fix flash flask form forms friends gae gallery games geek general gentoo gis git github gnome google google app engine gunicorn hackathon hacking hamburg heroku holidays hosting howto how-to how-tos html http i18n image install intermediate internet ios iphone java javascript jobs journalism jquery json justmigrated linear regression linkedin linode linux mac machine learning math memcached mercurial meta migration mirror misc model models mod_wsgi mongodb mozilla mvc mysql nelenschuurmans newforms news nginx nosql ogólne open source open-source orm osx os x ottawa paas performance philosophy php pi pil pinax pip piston planet plone plugin pony postgres postgresql ppoftw private programmieren programming programming &amp; internet project projects pycon pygrunn pyladies pypi pypy python python3 quick tips quora rabbitmq rails rant ratnadeep debnath redis release request resolutions rest review rtnpro ruby science script scripting security server setup simple smiley snaking software software development south sql ssh ssl static storage supervisor svn sysadmin tag talk nerdy to me tastypie tdd techblog technical technology template templates test testing tests tip tools tornado transifex travel tumbles tutorial twitter twoscoops typo3 ubuntu uncategorized unicode unix usergroup uwsgi uxebu virtualenv virtualenvwrapper web web 2.0 web design &amp; development webdev web development webfaction whoosh windows wordpress work