{ "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", " Educator News\n", "\n", " \n", "\n", " \n", "\n", " Educator News\n", "\n", " \n", "\n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This establishes the structure of every page we create from now on. When you look at the page you won't see anything different, but behind the scenes there is a proper html file structure.\n", "\n", "Let's add some more content and structure to our index page. Anything that is actually going to be part of every page will go in *base.html*, and anything that is unique to the home page will go in *index.html*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Building a top bar\n", "---\n", "This project is an attempt to build a version of Hacker News for the education profession. It is not a clone of HN, but it is directly inspired by HN. Many of the design decisions will be made by simply copying the parts of HN that work, with the mindset that we are free to modify any aspects of HN that should be updated or improved.\n", "\n", "One thing that we will certainly keep consistent with HN is the overall simplicity of the layout. So let's look at the top part of the HN home page, as it looks to a user who is not logged in:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![The HN home page, as it appears to a user who is not logged in.](/files/images/hn_home_page.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's only build the parts that are going to work immediately, so that as we build the site everything that is on the screen is functional. I prefer this approach rather than laying everything out, and then adding functionality. The first functionality we will implement is a login, so let's add that. The login link will appear on every page, so we will put it in *base.html* rather than *index.html*.\n", "\n", "In order to make this work, we need to jump into using css. Make a directory in the static folder to hold our css files, and make an empty *site_styles.css* file:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ mkdir educator_news/educator_news/static/css\n", "(venv)educator_news$ touch educator_news/educator_news/static/css/site_styles.css" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Django needs to know where to look for this project's static files, so make the following changes to *settings.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[8,9,10,11,12,13,14,15]\n", "...\n", "USE_L10N = True\n", "\n", "USE_TZ = True\n", "\n", "\n", "# Static files (CSS, JavaScript, Images)\n", "# https://docs.djangoproject.com/en/1.6/howto/static-files/\n", "\n", "STATIC_URL = '/static/'\n", "\n", "STATICFILES_DIRS = (\n", " os.path.join(DIRNAME, 'static'),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These lines tell Django to look in a directory called *static* within our *project* directory. You can store static files within an individual app as well, if you so choose." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The login link for HN allows users to log in or register; we will be a little more explicit and create a login/ register link on each page. Make the following changes to *base.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[9,10,11,17,18,19,20]\n", "\n", ",\n", "\n", " \n", " \n", " Educator News\n", "\n", "\t \n", "\t {% load staticfiles %}\n", "\t \n", "\n", " \n", "\n", " \n", "\n", "
\n", "\t\t
Educator News
\n", "\t\t\n", "\t
\n", "\n", " \n", "\n", "\n", " \n", " \n", " Educator News\n", "\n", "\t \n", "\t {% load staticfiles %}\n", "\t \n", "\n", " \n", "\n", " \n", "\n", "
\n", "\t\t
Educator News
\n", "\t\t\n", "\t
\n", "\n", " \n", "\n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, add the following to *site_styles.css*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "#project_title {\n", "\t font-size: 24px;\n", "}\n", "\n", "#login_links {\n", "\t text-align: right;\n", "}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now when you refresh [http://localhost:8000](http://localhost:8000), you should see something like the following:\n", "\n", "![Educator News homepage, with minimal styling.](/files/images/en_minimal_style.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using bootstrap for styling\n", "---\n", "I understand basic css, but it is not my strength. So twitter's bootstrap is perfect for me. If you like to do your own css work, you can keep the current structure of the project and skip to the section on implementing a login and registration system, styling as you go. If you are happy to use bootstrap in your projects, follow along with this section and start using bootstrap.\n", "\n", "If you like the idea of developing a project in a wireframe format first, you can skip all styling and just make sure all the functionality works. I know I want to deploy this project, so I want to be able to implement basic styling as we work through the project. I've done both methods, and with a project where I know pretty well what the final implementation should look like, I rather like having reasonable styling as I go. If this were a more original project and I were trying to work out the best functionality, I would probably go for a wireframe approach and leave styling until the project's functionality is more established.\n", "\n", "Implementing bootstrap is not that difficult now that we have static files configured for the local project. We need to download bootstrap, copy the correct files to *static/css* and *static/js*, and then choose a bootstrap template. This bootstrap template will go into the *base.html* file, and then we will be free to use any bootstrap styling on new elements." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Download bootsrap, and copy files to *static/css*\n", "\n", "First [download](http://getbootstrap.com/) the files for bootstrap. I prefer to download the source files to a directory outside of my project, and then move just the necessary bootstrap files to the actual project directory. There are some interesting resources in the bootstrap source distribution that are not in the smaller *dist* download.\n", "\n", "In your static folder, make a directory called *js* alongside your *css* folder. Copy some of the bootstrap files to your static directory, so that your static directory ends up looking like this:\n", "\n", "- static/css\n", " - boostrap.css\n", " - bootstrap.min.css\n", " - bootstrap-theme.css\n", " - bootstrap-theme.min.css\n", " - navbar-static-top.css\n", " - site_styles.css\n", "- static/js\n", " - bootstrap.js\n", " - bootstrap.min.js\n", " - jquery-1.10.2.min.js\n", " \n", "Bootstrap templates call jquery from a remote resource, but I like to have the file right in my project. There are a number of ways you can get the jquery file into your project. You can [download](http://jquery.com/download/) it from jquery directly. I pulled up the [source](view-source:http://getbootstrap.com/examples/navbar-static-top/) from a bootstrap sample page, clicked on the link to the [jquery resource](https://code.jquery.com/jquery-1.10.2.min.js), and copied the code directly into my *educator_news* project.\n", " \n", "Bootstrap offers a number of [sample templates](http://getbootstrap.com/getting-started/#examples) that you can start using in your project. For Educator News, I'm going with the [*Static top navbar*](http://getbootstrap.com/examples/navbar-static-top/) example. To use this example in your project, you copy the [source](view-source:http://getbootstrap.com/examples/navbar-static-top/) of the example, and modify the code as needed for your project. I chose the *Static top navbar* template for its simplicity.\n", "\n", "To start using the bootstrap template in our project, we need to:\n", "\n", "- Copy the source of the bootstrap theme we want into our *base.html* file.\n", "- Modify the *base.html* file by getting rid of any elements we don't want, and then adding in the content that is relevant to our project.\n", "- Make sure the links to bootstrap's static files are rewritten to work within our Django project.\n", "- Get rid of the syles we defined earlier in *site_styles.css*, but keep the empty file.\n", "\n", "To get started, here is the source for the *Static top navbar* bootstrap template. The highlighted sections are going to be removed completely. We will also get rid of or hide most of the navbar, just keeping the structure of the navbar." ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[20,21,46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,64,65,74,75,76,77,78,79,80,81,82]\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " Static Top Navbar Example for Bootstrap\n", "\n", " \n", " \n", "\n", " \n", " \n", "\n", " \n", " \n", "\n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "
\n", "
\n", "
\n", " \n", " Project name\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "
\n", "\n", " \n", "
\n", "

Navbar example

\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", "
\n", "\n", "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Copy and paste this into *base.html*, writing over the old version of the file.\n", "\n", "After removing the content from this template that we don't need, we have a simpler structure to start working with:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " Static Top Navbar Example for Bootstrap\n", "\n", " \n", " \n", "\n", " \n", " \n", "\n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "
\n", "
\n", "
\n", " \n", " Project name\n", "
\n", "
\n", "
    \n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "
\n", "\n", "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can add in our content:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[12,39,45,46]\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " Educator News\n", "\n", " \n", " \n", "\n", " \n", " \n", "\n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "
\n", "
\n", "
\n", " \n", " Educator News\n", "
\n", "
\n", "
    \n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "
\n", "\n", "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we modify the links to static resources, and add in a link to the *site_styles.css* file we created earlier:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 64, 65,66,67,68]\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " Educator News\n", " \n", " \n", "\t{% load staticfiles %}\n", " \n", "\t\n", " \n", " \n", " \n", " \n", " \n", "\t\n", " \n", "\n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "
\n", "
\n", "
\n", " \n", " Educator News\n", "
\n", "
\n", "
    \n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "
\n", "\n", "
\n", "\n", "\n", " \n", " \n", " \n", "\t\n", " \n", " \n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We don't need the two styles we defined earlier in *site_styles.css*, but we will want to write some of our own styles later on. So keep the file, but delete everything from it at this point so it is an empty file. Notice that we include *site_styles.css* after the bootstrap css files in *base.html*, so that we can override any bootstrap defaults we wish to.\n", "\n", "Now, when you visit [http://localhost:8000](http://localhost:8000) you should see the same file we had earlier, with a simple boostrap style:\n", "\n", "![Educator News, with a simple bootstrap style.](/files/images/en_homepage_bootstrap.png)\n", "\n", "The site is now responsive, as you can see by shrinking the window. If you shrink the window, the login and registration links shrink to a collapsed menu:\n", "\n", "![Educator News, showing the responsive styling.](/files/images/en_homepage_responsive.png)\n", "\n", "Now we can return to focusing on the functionality of the project, and style the project in a simple but responsive way as we make progress." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Authenticating users\n", "===\n", "Django provides a lot of default functionality for managing users, since it is a core need of any web app. We will use the default functionality whenever possible, to save ourselves some work.\n", "\n", "In this section we will focus on allowing users to log in and log out. We need to:\n", "\n", "- Create a User model.\n", "- Add the *ed_news* app to INSTALLED_APPS.\n", "- Migrate the database to incorporate this model.\n", "- Make a login page, and a logout view.\n", "- Update *base.html* to display different links for authenticated and unauthenticated users." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create a User model, and add it to the database\n", "---\n", "Django comes with a default User model, which includes such fields as email, first name, last name, and a few other fields. We are definitely going to have custom fields such as *karma*, so we will extend this default model from the start.\n", "\n", "Add the following to *ed_news/views.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "highlight=[2,3,4,5,6,7,8]\n", "from django.db import models\n", "from django.contrib.auth.models import User\n", "\n", "# --- Educator News models ---\n", "\n", "class UserProfile(models.Model):\n", " user = models.OneToOneField(User)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have a model defined, we need to add the *ed_news* app to `INSTALLED_APPS` in *settings.py*. Make the following changes to *settings.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[14,15]\n", "...\n", "INSTALLED_APPS = (\n", " 'django.contrib.admin',\n", " 'django.contrib.auth',\n", " 'django.contrib.contenttypes',\n", " 'django.contrib.sessions',\n", " 'django.contrib.messages',\n", " 'django.contrib.staticfiles',\n", "\n", " # Utilities/ libraries\n", " 'south',\n", "\n", " # My apps\n", " 'ed_news',\n", ")\n", "..." ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we need to update the database to include the *ed_news* app, and to include the *UserProfile* model. This is actually a *migration*, a move from one state of the database to another. The app *South* manages database migrations for us. Database migrations are often messy; South makes them as clean as they can be. From the *manage.py* directory, issue the following commands:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[2,6]\n", "(venv)educator_news$ python manage.py schemamigration ed_news --initial\n", " + Added model ed_news.UserProfile\n", "Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate ed_news\n", "\n", "(venv)educator_news$ python manage.py migrate ed_news\n", "educator_news$ python manage.py migrate ed_news\n", "Running migrations for ed_news:\n", " - Migrating forwards to 0001_initial.\n", " > ed_news:0001_initial\n", " - Loading initial data for ed_news.\n", "Installed 0 object(s) from 0 fixture(s)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now visit your project at [http://localhost:8000](http://localhost:8000) again, and you should not see any errors. You should not see anything different, but there should be no errors." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Making a login/ logout page\n", "---\n", "Django provides a [default login page](https://docs.djangoproject.com/en/1.6/topics/auth/default/#module-django.contrib.auth.views), which we will use to start this project. We won't really need to make a logout page, but we will have to implement a logout function.\n", "\n", "Let's start by updating the main *urls.py* file to include a link to the login and logout views:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[3,14,15,16]\n", "from django.conf.urls import patterns, include, url\n", "from django.core.urlresolvers import reverse\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", "\n", " # Auth urls\n", " url(r'^login/', 'django.contrib.auth.views.login', name='login'),\n", " url(r'^logout/', 'ed_news.views.logout_view', name='logout_view'),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To use Django's default login system, make a directory in *educator_news/static/templates/* called *registration*, and save a new template called *login.html* there:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

log in:

\n", "\n", " {% if form.errors %}\n", "

Your username and password didn't match. Please try again.

\n", " {% endif %}\n", "\n", "
\n", " {% csrf_token %}\n", "\n", "

{{ form.username.label_tag }}{{ form.username }}\n", "

{{ form.password.label_tag }}{{ form.password }}\n", "\n", "

\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", " \n", " Educator News\n", "
\n", "
\n", "
    \n", "
\n", "
    \n", "\n", "\t\t\t\t{% if user.is_authenticated %}\n", "
  • logout
  • \n", "\t\t\t\t{% else %}\n", "
  • login
  • \n", "
  • register
  • \n", "\t\t\t\t{% endif %}\n", "\n", "
\n", "
\n", "
\n", "
\n", "\n", "\n", "
\n", "\n", "\t\t{% block content %}\n", "\t\t{% endblock %}\n", "\n", "
\n", "\n", "\n", " \n", " \n", "\t \n", "\t \n", "\n", " \n", "" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now when you should be able to visit [http://localhost:8000](http://localhost:8000), and click on the login link. It should take you to a page that looks like this:\n", "\n", "![The Educator News login page, as provided by Django.](/files/images/en_login_page.png)\n", "\n", "You should be able to log in using the superuser credentials you created earlier. If you do this, you should be taken back to the home page, and you should no longer see the login and register links. In their place, you should see a link that you can click to log out:\n", "\n", "![When logged in, you see a link to logout.](/files/images/en_logged_in.png)\n", "\n", "When you click the logout link, you should see the original home page.\n", "\n", "Now we can move on to building a few pages that will allow users to manage their profile." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Making a profile page\n", "===\n", "Users of web apps need a way to view and manage their personal profiles. People need to verify and update email addresses, change passwords, and set options for their account. In this section, we will make a profile page, which will link to several other pages that allow users to take these management actions.\n", "\n", "First, let's modify the home page to show a user's username when they are logged in. This username will link to their profile page. When a user is logged in, you can access their username through the variable *{{ user.username }}*. Let's add this to the *base.html* file:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[20]\n", "\n", "
\n", "
\n", "
\n", " \n", " Educator News\n", "
\n", "
\n", "
    \n", "
\n", " \n", "
\n", "
\n", "
" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's make a profile page that this will link to. Make a new file, *educator_news/static/templates/registration/profile.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

Username: {{ user.username }}

\n", "

First Name: {{ user.first_name }}

\n", "

Last Name: {{ user.last_name }}

\n", "

Email: {{ user.email }}

\n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add this page to *urls.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[17]\n", "from django.conf.urls import patterns, include, url\n", "from django.core.urlresolvers import reverse\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", "\n", " # Auth urls\n", " url(r'^login/', 'django.contrib.auth.views.login', name='login'),\n", " url(r'^logout/', 'ed_news.views.logout_view', name='logout_view'),\n", " url(r'^profile/', 'ed_news.views.profile', name='profile'),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add a new view to *views.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[20,21,22,23]\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('/')\n", "\n", "def profile(request):\n", " return render_to_response('registration/profile.html',\n", " {},\n", " context_instance = RequestContext(request))" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now when you are logged in, you should see your username in the header:\n", "\n", "![Username appears in header](/files/images/en_username_header.png)\n", "\n", "When you click on your username, you should see your user profile:\n", "\n", "![User profile](/files/images/en_profile_page.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can add the ability to change a user's password." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Letting users change their password\n", "---\n", "First, let's make a link to a password change page, on the profile page:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[11]\n", "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

Username: {{ user.username }}

\n", "

First Name: {{ user.first_name }}

\n", "

Last Name: {{ user.last_name }}

\n", "

Email: {{ user.email }}

\n", "\n", "

Change password

\n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's make a page where the user will enter their new password. This will be called *password_change_form.html*, and it should go in the *educator_news/static/templates/registration/* directory:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

Change password:

\n", "

This page allows you to change your password.

\n", "\n", "\n", " {% if form.errors %}\n", "

Please re-enter your passwords. You either entered the wrong current password, or your new passwords did not match.

\n", " {% endif %}\n", "\n", "
\n", " {% csrf_token %}\n", "\n", "

old password: \n", "

new password: \n", "

new password: \n", " \n", "

\n", "

\n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's make the view for the password change page. The page will redirect to itself if there is an issue such as new passwords that don't match, or an incorrect entry of the current password. If the change is successful, the user will be redirected to a page confirming the success. This will be *password_change_successful*." ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[6,26,27,28,29,30,31,32,35,36,37,38]\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", "from django.contrib.auth.views import password_change\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('/')\n", "\n", "def profile(request):\n", " return render_to_response('registration/profile.html',\n", " {},\n", " context_instance = RequestContext(request))\n", "\n", "def password_change_form(request):\n", " if request.method == 'POST':\n", " return password_change(request, post_change_redirect='/password_change_successful')\n", " else:\n", " return render_to_response('registration/password_change_form.html',\n", " {},\n", " context_instance = RequestContext(request))\n", "\n", "\n", "def password_change_successful(request):\n", " return render_to_response('registration/password_change_successful.html',\n", " {},\n", " context_instance = RequestContext(request))" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we need to make a template for *password_change_successful*, saved as *educator_news/static/templates/registration/password_change_successful.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

Password changed.

\n", "\n", "

Return to profile page.

\n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we need to update the main *urls.py* to include these pages:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[18,19]\n", "from django.conf.urls import patterns, include, url\n", "from django.core.urlresolvers import reverse\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", "\n", " # Auth urls\n", " url(r'^login/', 'django.contrib.auth.views.login', name='login'),\n", " url(r'^logout/', 'ed_news.views.logout_view', name='logout_view'),\n", " url(r'^profile/', 'ed_news.views.profile', name='profile'),\n", " url(r'^password_change/', 'ed_news.views.password_change_form', name='password_change_form'),\n", " url(r'^password_change_successful/', 'ed_news.views.password_change_successful', name='password_change_successful'),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, you should be able to click on a link on your profile, and change your password:\n", "\n", "![Password change page.](/files/images/en_password_change.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can make a registration page, where new users can make an account for themselves." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Make a registration page\n", "===\n", "We now have a way for existing users to log in, but there is no way for new users to make an account. Let's make a page that lets people register an account. We will not deal with email registration at this point; anyone can make an account, and log in immediately.\n", "\n", "- Make a link from the home page.\n", "- Make a set of forms for User and UserProfile\n", "- Make a view for the register page.\n", "- Make a page to display the registration forms.\n", "- Make an entry for the registration page url.\n", "\n", "Add a link to the registration page on *base.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[25]\n", "...\n", " \n", "
\n", "
\n", "
\n", " \n", " Educator News\n", "
\n", "
\n", "
    \n", "
\n", " \n", "
\n", "
\n", "
\n", "..." ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we need a form for the registering person to enter their user information. This will reference the User class, but we also need to connect a UserProfile to this User class.\n", "\n", "Make a new file *ed_news/forms.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from django import forms\n", "from django.contrib.auth.models import User\n", "from ed_news.models import UserProfile\n", "\n", "class UserForm(forms.ModelForm):\n", " password = forms.CharField(widget=forms.PasswordInput())\n", "\n", " class Meta:\n", " model = User\n", " fields = ('username', 'password')\n", "\n", "class UserProfileForm(forms.ModelForm):\n", " class Meta:\n", " model = UserProfile\n", " fields = ()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add the following to *views.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[7,41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84]\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", "from django.contrib.auth.views import password_change\n", "from ed_news.forms import UserForm, UserProfileForm\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('/')\n", "\n", "def profile(request):\n", " return render_to_response('registration/profile.html',\n", " {},\n", " context_instance = RequestContext(request))\n", "\n", "def password_change_form(request):\n", " if request.method == 'POST':\n", " return password_change(request, post_change_redirect='/password_change_successful')\n", " else:\n", " return render_to_response('registration/password_change_form.html',\n", " {},\n", " context_instance = RequestContext(request))\n", "\n", "\n", "def password_change_successful(request):\n", " return render_to_response('registration/password_change_successful.html',\n", " {},\n", " context_instance = RequestContext(request))\n", "\n", "def register(request):\n", " # Assume registration won't work.\n", " registered = False\n", "\n", " if request.method == 'POST':\n", " user_form = UserForm(data=request.POST)\n", " profile_form = UserProfileForm(data=request.POST)\n", "\n", " if user_form.is_valid() and profile_form.is_valid():\n", " # Save user's form data.\n", " user = user_form.save()\n", "\n", " user.set_password(user.password)\n", " user.save()\n", "\n", " profile = profile_form.save(commit=False)\n", " profile.user = user\n", " profile.save()\n", "\n", " # Registration was successful.\n", " registered = True\n", "\n", " # Not requiring email validation yet, so log user in.\n", " #user = authenticate(username=user.username, password=user.password)\n", " #login(request, user)\n", " print 'un, pw', user.username, user.password\n", "\n", " else:\n", " # Invalid form/s.\n", " # Print errors to console; should log these?\n", " print 'ufe', user_form.errors\n", " print 'pfe', profile_form.errors\n", "\n", " else:\n", " # Send blank forms.\n", " user_form = UserForm()\n", " profile_form = UserProfileForm()\n", "\n", " return render_to_response('registration/register.html',\n", " {'user_form': user_form,\n", " 'profile_form': profile_form,\n", " 'registered': registered,\n", " },\n", " context_instance = RequestContext(request))" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Add a new file *educator_news/static/templates/registration/register.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

Register:

\n", "

This page allows you to register a new account.

\n", "\n", "\t {% if registered %}\n", "\t

Thank you for registering!

\n", "\t

Return to the homepage.

\n", "\n", "\t {% else %}\n", "\t\t
\n", "\t\t\t {% csrf_token %}\n", "\n", "\t\t\t {{ user_form.as_p }}\n", "\t\t\t {{ profile_form.as_p }}\n", "\n", "\t\t\t

\n", "\t\t

\n", "\t \n", "\t {% endif %}\n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, modify *urls.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[20]\n", "from django.conf.urls import patterns, include, url\n", "from django.core.urlresolvers import reverse\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", "\n", " # Auth urls\n", " url(r'^login/', 'django.contrib.auth.views.login', name='login'),\n", " url(r'^logout/', 'ed_news.views.logout_view', name='logout_view'),\n", " url(r'^profile/', 'ed_news.views.profile', name='profile'),\n", " url(r'^password_change/', 'ed_news.views.password_change_form', name='password_change_form'),\n", " url(r'^password_change_successful/', 'ed_news.views.password_change_successful', name='password_change_successful'),\n", " url(r'^register/', 'ed_news.views.register', name='register'),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, new users can register an account:\n", "\n", "![New user registration page.](/files/images/en_registerhtml.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After registration, the user will have an entry in our database for the User model, as well as the UserProfile model. Now we can take a look at the admin site, and see who our users are." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using the admin site\n", "===\n", "One of the nice changes in Django 1.6 is that the admin site is set up automatically. The admin site is a collection of pages that makes it relatively easy for us as the developers of a project to access our data without having to write a bunch of code to do so. We basically register our models with the admin site, and the pages are created automatically for us. The User model is already included in the admin by default, so let's register the UserProfile model, and have a look at the data we have so far.\n", "\n", "Modify *ed_news/admin.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[3,5]\n", "from django.contrib import admin\n", "from ed_news.models import UserProfile\n", "\n", "admin.site.register(UserProfile)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now if you go to [http://localhost:8000/admin](http://localhost:8000/admin) and log in using the superuser account you created earlier, you should be able to explore your data. Here is what I have so far for the UserProfile model:\n", "\n", "![The admin site, showing the UserProfiles that have been created so far.](/files/images/en_admin_userprofile.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The admin pages can be customized, but they are not really meant to be user-facing. One simple use is to delete these kind of test accounts from your database as you are developing your project." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Building a submit page\n", "===\n", "The first piece of specific functionality we need is the ability for users to submit articles. To build this functionality, we will:\n", "\n", "- Update *models.py* to include a model for submissions.\n", "- Migrate the database to include these new models.\n", "- Make a link to the *submit* page.\n", "- Make an entry in *ed_news/urls.py* for the *submit* page.\n", "- Write a form for submitting an article.\n", "- Write a view for the submit page.\n", "- Write the template for the *submit.html* page.\n", "\n", "Add this to *models.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "...\n", "class Submission(models.Model):\n", " \"\"\"An abstract class for the two types of submission,\n", " which are an article and a text submission. Text submissions\n", " can be questions, ie Ask EN, or posts such as Show EN.\n", " \"\"\"\n", "\n", " # Stick with programming 80-char limit for now.\n", " # It's what HN uses, which fits nicely on mobile.\n", " title = models.CharField(max_length=80)\n", " author = models.ForeignKey(User)\n", " upvotes = models.IntegerField(default=0)\n", " downvotes = models.IntegerField(default=0)\n", " points = models.IntegerField(default=0)\n", " submission_time = models.DateTimeField(auto_now_add=True)\n", "\n", " class Meta:\n", " abstract = True\n", "\n", " def __unicode__(self):\n", " return self.title\n", "\n", "class Article(Submission):\n", " url = models.URLField()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Migrate the db again:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "(venv)educator_news$ python manage.py schemamigration --auto ed_news\n", "(venv)educator_news$ python manage.py migrate ed_news" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Include a link to the submit page in *base.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[16]\n", "\n", "
\n", "
\n", "
\n", " \n", " Educator News\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", "
" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Modify *ed_news/urls.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[11]\n", "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", " url(r'^submit/', views.submit, name='submit'),\n", ")" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Modify *forms.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[5,19,20,21,22]\n", "from django import forms\n", "from django.contrib.auth.models import User\n", "from ed_news.models import UserProfile\n", "from ed_news.models import Article\n", "\n", "class UserForm(forms.ModelForm):\n", " password = forms.CharField(widget=forms.PasswordInput())\n", "\n", " class Meta:\n", " model = User\n", " fields = ('username', 'password')\n", "\n", "class UserProfileForm(forms.ModelForm):\n", " class Meta:\n", " model = UserProfile\n", " fields = ()\n", "\n", "class ArticleForm(forms.ModelForm):\n", " class Meta:\n", " model = Article\n", " fields = ('title', 'url',)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Modify *views.py*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "###highlight=[9,13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46]\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", "from django.contrib.auth.views import password_change\n", "\n", "from ed_news.forms import UserForm, UserProfileForm\n", "from ed_news.forms import ArticleForm\n", "...\n", "\n", "...\n", "# --- Educator News views ---\n", "def submit(request):\n", "\n", " submission_accepted = False\n", " if request.method == 'POST':\n", " article_form = ArticleForm(data=request.POST)\n", "\n", " if article_form.is_valid():\n", " print 'af', article_form\n", " print 'afcd', article_form.cleaned_data\n", " print 'u', request.user\n", " print 'uid', request.user.id\n", " print 'utype', type(request.user)\n", " print article_form.cleaned_data['url']\n", "\n", " article = article_form.save(commit=False)\n", " article.author = request.user\n", " article.save()\n", " submission_accepted = True\n", "\n", " else:\n", " # Invalid form/s.\n", " # Print errors to console; should log these?\n", " print 'ae', article_form.errors\n", "\n", " else:\n", " # Send blank forms.\n", " article_form = ArticleForm()\n", "\n", " return render_to_response('ed_news/submit.html',\n", " {'article_form': article_form,\n", " 'submission_accepted': submission_accepted,\n", " },\n", " context_instance = RequestContext(request))" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Make a new file *submit.html*:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "{% extends 'base.html' %}\n", "\n", "{% block content %}\n", "\n", "

Submit

\n", "

You can use this form to submit an article.

\n", "\n", "\t {% if user.is_authenticated and submission_accepted %}\n", "\t

Thank you for your submission!

\n", "\n", "\t {% elif user.is_authenticated %}\n", "\t

Submit form

\n", "\t\t
\n", "\t\t\t {% csrf_token %}\n", "\n", "\t\t\t {{ article_form.as_p }}\n", "\n", "\t\t\t

\n", "\t\t

\n", "\n", "\t {% else %}\n", "\t\t

You have to log in or register if you would like to make a submission.

\n", "\n", "\t {% endif %}\n", "\n", "{% endblock %}" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now you should be able to submit an article:\n", "\n", "![Submit page.](/files/images/en_submit_page.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can move on to building a page to show the newest submissions." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[top](#)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- - -\n", "[Home](http://nbviewer.ipython.org/urls/raw.github.com/ehmatthes/intro_programming/master/notebooks/index.ipynb)" ] } ], "metadata": {} } ] }