{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Educator News - A Django Tutorial\n", "===\n", "I have learned a lot from reading [Hacker News](http://news.ycombinator.com) over the last few years, and I have wished there was a similar forum for gathering and discussing articles related to education. I have seen at least one attempt at making a clone of HN for education, but the project did not go anywhere. This is an attempt to make an HN-inspired site for education. In the process, you will see how to build, deploy, and maintain a web application using the Django framework." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[Home](http://nbviewer.ipython.org/urls/raw.github.com/ehmatthes/intro_programming/master/notebooks/index.ipynb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Contents\n", "===\n", "- [Overview and Specifications](#overview)\n", " - [One sentence](#one_sentence)\n", " - [Elevator pitch](#elevator_pitch)\n", " - [Specifications](#specifications)\n", " - [Educator News - what users see](#users_see)\n", " - [Development outline](#development_outline)\n", " - [Other notes](#other_notes)\n", "- [Starting the project](#starting_project)\n", " - [Setting up a virtual environment](#setting_up_environment)\n", " - [Initial commit](#initial_commit)\n", "- [Building the app](#building_app)\n", " - [Creating an index page](#index_page)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Overview\n", "===\n", "When creating a new project that you are excited about, it is tempting to jump right into writing code. This is rarely a recipe for success; it is almost always good to put some of your ideas and goals into writing first. Before writing any code it is good to lay out a clear vision, and then lay out a series of goals that you aim to achieve. This will help guide you into developing the product you want, rather than being pulled into something that is easy or satisfying to code but may be less useful." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One sentence\n", "---\n", "It is helpful to come up with a one-sentence description of your project. Here is a one-sentence description of Educator News:\n", "\n", "#### \"Educator News is a place to share and discuss articles of interest to educators, with a focus on improving the professionalism of the field as a whole.\"\n", " \n", "This sentence, if written carefully, can help you evaluate whether you are staying true to your original vision as you begin to write code." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Elevator pitch\n", "---\n", "An *elevator pitch* is a slightly longer description that still gets at the heart of what the project should be. It answers the question, \"If you were in an elevator for a brief ride with someone you really wanted on board your project, what would you say?\" Here is an elevator pitch for Educator News:\n", "\n", "#### \"A professional sharing of ideas between educators is good for everyone. It allows teachers to do the best job possible, and it allows their students to have the best opportunity for learning. Most online communities for educators, however, devolve into online versions of a bad teachers' lounge. They are very negative, with a constant focus on complaints with little constructive and professional thinking. Educator News aims to help teachers and people interested in education share high-quality articles about the profession of teaching, and engage in constructive conversations about how to increase the professionalism of the education field.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Specifications\n", "---\n", "Good programming projects have a clear set of specifications that need to be met. These specifications are often open to revision as the project moves forward, but there should be a clear road map that outlines critical features and design parameters. It tells you what users of a project will see, and what they will be able to do." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Educator News - What users see\n", "---\n", "Here are the major goals of the Educator News project:\n", "\n", "- A landing page will tell people what Educator News is, and why they should get involved.\n", " - It will allow people to register.\n", " - It will link to the \"front page\" of articles.\n", "- A \"submit\" page will allow registered users to submit new articles.\n", " - Users can submit a url and title.\n", "- A \"new\" page will list the newest articles that have been submitted.\n", " - Any registered user can upvote an article.\n", " - Registered users with enough karma can downvote an article.\n", " - Any registered user can comment on an article.\n", "- A \"front page\" will list the top articles that have been submitted." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Development outline\n", "---\n", "The overall steps to make this happen are roughly the following:\n", "\n", "- Set up a Django project within a virtual environment (virtualenv).\n", "- Develop the user model.\n", "- Build a landing page, allowing users to register.\n", "- Deploy the landing page.\n", " - This is mostly to start a realistic cycle of develop -> deploy -> revise -> deploy.\n", " - In most projects, you'd do more local development before bothering to deploy.\n", " - Use twitter bootstrap for styling.\n", "- Develop the article model.\n", "- Build the \"submit\" page.\n", " - Deploy the submit page.\n", "- Build the \"new\" page.\n", " - Deploy the new page.\n", "- Build the \"front\" page.\n", " - Deploy the front page.\n", "- Share with a small audience.\n", " - Gauge initial interest.\n", " - Gather initial feedback.\n", "- Identify rest of needed features\n", " - Build and deploy.\n", "- Identify and start to implement other infrastructure:\n", " - Monitoring traffic\n", " - Issue tracker\n", " - Backup routines\n", " - Fighting spam\n", " - Montoring quality of submissions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Other notes\n", "---\n", "A few other notes:\n", "\n", "- This is an open-source project, which will be hosted on Github.\n", "- This is a non-profit project. It will be free, and users will not see any ads." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Starting the project\n", "===\n", "Some of the terminology can be a little confusing when you first start working with Django. A site that is built in Django is called a *project*, and a project can be made up of any number of *apps*. Every project has at least one app.\n", "\n", "When you start building a project in Django, your project will use a number of different packages. It is quite possible your system will evolve over the life of your project, so that your system's packages no longer match the packages you used in your Django project. To manage these kinds of issues, it is strongly recommended that you do your project work in a virtual environment.\n", "\n", "A virtual environment is an isolated environment on your system that has a specific set of packages. This way you can even have projects that use different versions of Django on the same computer. This also allows you to choose when to upgrade the packages you use for a project, based on that particular project's schedule. You can update each project on its own timeframe, rather than having to upgrade everything on your system at once. You can also easily help other developers create an environment that is identical to your own, so that everyone involved in a project can be working with the exact same set of packages." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Setting up a virtual environment\n", "---\n", "First off, let's set up a virtual environment to contain this project:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "$ mkdir -p /srv/projects/educator_news\n", "$ cd /srv/projects/educator_news\n", "$ virtualenv venv\n", "$ source venv/bin/activate\n", "(venv)$ pip install Django psycopg2 South dj-database-url\n", "(venv)$ django-admin.py startproject educator_news\n", "(venv)$ cd educator_news\n", "(venv)educator_news$ python manage.py runserver\n", "...\n", "Starting development server at http://127.0.0.1:8000/\n", "Quit the server with CONTROL-C." ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If this worked, you will see a browser tab showing an active, empty Django project:\n", "\n", "![An empty project in Django](/files/images/django_it_worked.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Initial commit\n", "---\n", "Now let's make an initial commit on the project." ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ cd ..\n", "(venv)$ touch .gitignore" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add the following lines to the `.gitignore` file. I tend to have a *notes* directory in many of my project directories, where I keep some working notes. I don't want these committed to my project, so they appear in my *.gitignore*. You can leave this line out if you don't have a *notes* directory.\n", "\n", " venv/*\n", " *.pyc\n", " notes/*\n", " \n", "This file makes sure that git does not track all the files in your virtualenv. That is not necessary, because you can always recreate your virtualenv easily if you are maintaining it properly. You don't need the *.pyc* files, because they are automatically generated." ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)$ git add .\n", "(venv)$ git commit -am \"Initial commit.\"" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Setting up the database\n", "---\n", "Django will need access to a database in order to store all the information it receives. For this project, we are going to use Postgres. It doesn't matter what directory you do these steps from." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Create a user for the database\n", "Make sure you decline the superuser role, allow the new user to create databases, and disallow the user from creating new roles/ users." ] }, { "cell_type": "code", "collapsed": false, "input": [ "$ # Create a user for this project.\n", "$ sudo -u postgres createuser -P django_user_ednews\n", "Enter password for new role: \n", "Enter it again: \n", "Shall the new role be a superuser? (y/n) n\n", "Shall the new role be allowed to create databases? (y/n) y\n", "Shall the new role be allowed to create more new roles? (y/n) n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Create a database for the project\n", "We need to create a database for this project, and make sure the user we just created owns the database. To create the database, perform the following steps:\n", "\n", "- Switch to the *postgres* user;\n", "- Start a postgres terminal session;\n", "- Create the database;\n", "- Exit the postgres terminal session;\n", "- Switch back to your normal username.\n", "\n", "Here is what these steps look like:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "$ su postgres\n", "Password: \n", "$ psql template1\n", "psql (9.1.10)\n", "Type \"help\" for help.\n", "\n", "template1=# CREATE DATABASE ednews_db OWNER django_user_ednews ENCODING 'UTF8';\n", "CREATE DATABASE\n", "template1=# \\q\n", "$ su ehmatthes\n", "Password: \n", "$ " ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Configure the database\n", "Postgres needs to be reconfigured to allow the new user to have access to the new database.\n", "\n", "- Back up the original postgres configuration file.\n", "- Modify the configuration file to allow the new user access. This consists of adding a line of the form `local *database_name* *database_user_name* md5`.\n", "- Restart postgres." ] }, { "cell_type": "code", "collapsed": false, "input": [ "$ sudo cp /etc/postgresql/9.1/main/pg_hba.conf /etc/postgresql/9.1/main/pg_hba.conf.original\n", "$ sudo nano /etc/postgresql/9.1/main/pg_hba.conf\n", "# Add the following line, after a similar line.\n", "# Don't delete any lines from pg_hba.conf\n", "local ednews_db django_user_ednews md5\n", "$ sudo /etc/init.d/postgresql restart" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Configure Django to use the database\n", "Open *settings.py*. Remove or comment out the DATABASES dictionary, and replace it with these two lines:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import dj_database_url\n", "DATABASES = {'default': dj_database_url.config() }" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Make a *.env* file in the main project directory (at the same level as *venv*):" ] }, { "cell_type": "code", "collapsed": false, "input": [ "DATABASE_URL=postgres://django_user_mysite:password@localhost/mysite_db" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add the following lines to the end of the */venv/bin/activate* script:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# Use my env variables\n", "export $(cat /srv/projects/mysite/.env)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Modify your *.gitignore* file, so that your environment variables do not get accidentally commited to a repository and pushed to a public repository." ] }, { "cell_type": "code", "collapsed": false, "input": [ "# .gitignore\n", "venv/*\n", "*.pyc\n", "notes/*\n", ".env" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Run syncdb once to create the initial tables. With an active virtual environment:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ python manage.py syncdb" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You will be prompted to create a superuser. Say yes. I use \u201cmysite_su\u201d for the superuser name. You can now refresh [http://localhost:8000](http://localhost:8000) in your browser, and you should see the same empty Django page." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Adding South to the project\n", "There is one more thing we will do to set up our local project, before starting to code the actual site. We will set up South to handle changes to our database. Edit the *settings.py* file so the list of INSTALLED_APPS includes south." ] }, { "cell_type": "code", "collapsed": false, "input": [ "INSTALLED_APPS = (\n", " 'django.contrib.auth',\n", " ...\n", "\n", " # Utilities/ libraries\n", " 'south',\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we need to run syncdb one more time to install the tables South will need to manage our database. This is the **last time we will use syncdb**; after this we will put South in charge of managing changes to the database." ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ python manage.py syncdb" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Visit your project in your browser at [http://localhost:8000](http://localhost:8000), and you should still see the empty Django project with no error messages." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Building the app\n", "===\n", "Django distinguishes between *projects* and *apps*, which can be a little confusing. You probably think of your entire project as a single app. Django considers an *app* a specific bit of functionality in your *project*. The point of Django's distinction between projects and apps is to allow you to develop reusable *apps* that can be used within different *projects*. So if you have a blog *app*, you can use it in any number of your projects. The people who maintain Django want you to be able to share your apps with other developers, and use common apps within your larger project.\n", "\n", "This overall project is called *educator_news*. It is fine to write your overall project as a single app, especially if you are just starting out in web development, and are unsure how to break up a project into smaller apps. However, you can't have an app with the same name as your project, otherwise there would be naming conflicts in the actual code.\n", "\n", "Let's make an app called *ed_news*. This will contain all of our models and page views.\n", "\n", "From the directory that contains *manage.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ python manage.py startapp ed_news" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This creates an *app* called *ed_news*, which is a part of the *educator_news* project. Most of the work we have to do will take place within the ed_news app, but we will also deal with some settings and project files in the educator_news project." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creating an index page\n", "---\n", "There is a fair bit of work we have to do in order to get an index page displayed. This is routine work, so once you have built a few projects in Django this setup work will go very quickly. To get an initial static index page displayed, we need to:\n", "\n", "- Modify *urls.py* so that ed_news urls map to their proper pages.\n", "- Create a *urls.py* within the ed_news app, so that we can properly map all of the urls that will be part of the ed_news app. The root url, and the root url with /index needs to map to our index page.\n", "- Write a function in *ed_news/views.py* that tells Django how to build the index page.\n", "- Create an *educator_news/static/templates/ed_news* path, to hold our templates. Within this directory, we will need an *index.py* file, which is the template for the index page.\n", "- Modify *settings.py* so that Django knows where to find the templates for this project.\n", "\n", "#### Modify *urls.py*\n", "\n", "Django maps each url to a view function, which then calls a template that is rendered as an html page. There is a file in the overall project called *urls.py*, which responds to the url request from each page. You could put all of your url designations in this file, but it is cleaner to put specific urls within the app directory. So, the project *urls.py* file needs to make a call to the app *urls.py* file. Here is what the project *urls.py* file should look like:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[8,9]\n", "from django.conf.urls import patterns, include, url\n", "\n", "from django.contrib import admin\n", "admin.autodiscover()\n", "\n", "urlpatterns = patterns('',\n", " # My urls\n", " url(r'^', include('ed_news.urls', namespace='ed_news')),\n", "\n", " url(r'^admin/', include(admin.site.urls)),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The highlighted line pulls in any urls that are mapped in the ed_news app into the overall project. This frees you to define urls within the app itself.\n", "\n", "#### Create a *urls.py* file within the ed_news app\n", "\n", "When we created the ed_news app with `python manage.py startapp ed_news`, some files were created automatically for us. A project-specific *urls.py* file is not made automatically. So create a new file in the *ed_news* directory, and enter the followig lines:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from django.conf.urls import patterns, url\n", "\n", "from ed_news import views\n", "\n", "urlpatterns = patterns('',\n", " # My urls\n", "\n", " # --- Educator News home page ---\n", " url(r'^$', views.index, name='index'),\n", "\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This file maps one url to a view function. It maps the url /index to the index view. This means if someone requests the url `http://root_domain/index`, this view will be called. Let's create that view.\n", "\n", "#### Create an index function in *views.py*\n", "Open the *ed_news/views.py* file, and enter the following lines:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from django.shortcuts import render_to_response\n", "from django.template import RequestContext\n", "\n", "def index(request):\n", " # Static index page for now.\n", " return render_to_response('ed_news/index.html',\n", " {},\n", " context_instance = RequestContext(request))" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This imports the function *render_to_response()*, and the class *RequestContext*. It defines a view function called *index*, which maps to the *index.html* template. The index page does not need any custom information yet, so an empty dictionary is sent to *render_to_response()*. Sending a *context_instance* will make it easier to include forms and dynamic data on the pages shortly.\n", "\n", "#### Make an *index.html* template\n", "You need to make an actual template for the *index.html* page. This template needs to live somewhere, and Django needs to know how to find it. From the overall project directory, make a place for static files to live. Within that directory, make an empty file called *index.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ mkdir -p educator_news/static/templates/ed_news\n", "(venv)educator_news$ touch educator_news/static/templates/ed_news/index.html" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This file will build the home page for our project. The *index.html* file will contain one line for the moment:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "Educator News" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Modify *settings.py* to include templates\n", "Django needs to know where to find your template files. Add the following lines to *settings.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[10,11,12,13]\n", "# SECURITY WARNING: don't run with debug turned on in production!\n", "DEBUG = True\n", "\n", "TEMPLATE_DEBUG = True\n", "\n", "ALLOWED_HOSTS = []\n", "...\n", "\n", "DIRNAME = os.path.dirname(__file__)\n", "TEMPLATE_DIRS = (\n", " os.path.join(DIRNAME, 'static/templates'),\n", ")\n", "\n", "# Application definition\n", "\n", "INSTALLED_APPS = (\n", " 'django.contrib.admin',\n", " 'django.contrib.auth',\n", " 'django.contrib.contenttypes',\n", "..." ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### View the *index.html* page\n", "Now when you look at the page [http://localhost:8000/](http://localhost:8000), you should see a simple \"Educator News\" home page instead of the default Django homepage:\n", "\n", "![A simple Educator News homepage](files/images/ed_news_homepage.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If this worked, congratulations. It's as simple as an html page can get, but it's your first page rendered through Django. Now let's make it a little more interesting." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create a *base.html* template\n", "---\n", "Django helps you build coherent projects by allowing templates to inherit from one another. So before going any further, it would be good to create a *base.html* template, and have all pages inherit from this template. This base template will contain the boilerplate structure of a proper html file, and it will contain the parts of the page that are common to every page on the site: the navigation, the branding, and the overall page structure.\n", "\n", "Make a file called *base.html*, and place it directly inside the *templates* directory you just created:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ touch educator_news/educator_news/static/templates/base.html" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For the moment, just put the following in *base.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "Educator News" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Modify *index.html* so that it inherits from *base.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends base.html %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now visit [http://localhost:8000](http://localhost:8000) again, and you should see a simple page with the text \"Educator News\" on it just as you did before.\n", "\n", "Modern browsers will attempt to build an html page even if it lacks proper structure, but it's not good practice to build such a page. Let's modify *base.html* to have a proper html structure:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "\n", "\n", "
\n", " \n", "This example is a quick exercise to illustrate how the default, static and fixed to top navbar work. It includes the responsive CSS and HTML, so it also adapts to your viewport and device.
\n", "To see the difference between static and fixed top navbars, just scroll.
\n", "\n", " View navbar docs »\n", "
\n", "Your username and password didn't match. Please try again.
\n", " {% endif %}\n", "\n", " \n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We need a view to call the logout function. Add the following to *views.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[4,5,14,15,16,17,18]\n", "from django.shortcuts import render_to_response, redirect\n", "from django.template import RequestContext\n", "from django.contrib.auth import logout\n", "from django.core.urlresolvers import reverse\n", "\n", "def index(request):\n", " # Static index page for now.\n", " return render_to_response('ed_news/index.html',\n", " {},\n", " context_instance = RequestContext(request))\n", "\n", "\n", "# Authentication views\n", "def logout_view(request):\n", " logout(request)\n", " # Redirect to home page.\n", " return redirect('/')" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we need to modify *base.html* and make the login and logout links active. This listing shows just the body, since there are no changes needed for the head section of the page:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[14,21,22,23,24,25,26]\n", "\n", "\n", " \n", " \n", "\n", "\n", "