Let's Take Notes With Django - Part 4

Let's Take Notes With Django - Part 4

part of the Startproject To Deployment - Django Tutorial series

show all in series

back to index

Now that we're through Part 3 of the tutorial, we've got the beginnings of a living website. What a time to be alive! Right now it's just a single static page, but by the end of part 4 it'll be so, so much more.

We talked briefly last time about these two lines in our core urls.py file:

urlpatterns = patterns('',
url(r'^$', home_view, name='home'),
url(r'^notes/', include('notes.urls', namespace='notes')),
)

Each pattern has three important pieces of information:


  1. A regular expression,

  2. Instructions on what to do when encountering a URL that matches a particular regular expression, and

  3. A handy name or namespace for the pattern

The regular expression matches what comes after our root address. We're running everything locally right now, which means your DjangoNote homepage shows up in your browser after runserver when you point to 127.0.0.1. Consider the two patterns we have defined: 127.0.0.1 matches the first pattern because nothing comes after the root address. Once a match to that pattern is detected, Django responds by serving up the response returned by our home_view function. If we instead browse to 127.0.0.1/notes, Django will see that we've "included" the notes/urls.py file to handle the request. Of course, we haven't defined any patterns in that urls.py file yet, which is why that address currently gives us this helpful message:

ImproperlyConfigured at /notes
The included urlconf '' does not appear to have any patterns in it. If you see valid patterns in the file then the issue is probably caused by a circular import.

As you remember, our ultimate goal here is to have a sweet note-taking application that allows us to log in and privately record our deepest secrets from time to time. We're also going to add in the ability to "tag" notes with different tags, which will help us stay organized. With that in mind, let's think about the functionality we'll need to build in our notes app. Once we log in and navigate to 127.0.0.1/notes, we should have the ability to:


  1. See all of our existing notes

  2. Add a new note

  3. Edit and delete existing notes

  4. See all of our existing tags

  5. Create new tags

  6. Edit and delete existing tags

  7. Some basic search capability - find notes with a given tag, for instance

We'll need to create views and URL patterns for all of these inside our notes app. And remember, we want all of this stuff to be accessible only to us - the admin / superuser of the website. That means we need a way to log in, check user authentication, and "gate" our private content to keep the riff-raff out. That stuff doesn't really sound like it has much to do with notes, and if we added some more apps later we might want to log in to those, too. So instead of putting this login logic inside our notes app, let's keep it inside our core project logic so that all of our apps can use it.

You're going to love how easy this is. Django includes a base User model with an authentication and session management system already built in, so a good bit of our work has already been done for us. You've actually already created a User and probably didn't even realize it - remember when you ran python manage.py createsuperuser during our initial database setup? The superuser you created was our first User - let's go look at it.

Open up your Terminal and start the Django shell with python manage.py shell_plus. This assumes you've installed django_extensions like we talked about back in Part 1 of this series. If you haven't, or don't want to for whatever reason, you can use shell instead of shell_plus. Just be aware that using shell_plus automatically imports about a billion things for you when you start up the shell and is going to save you a few minutes of setup time every time you want to muck around with some code. This is what I see when I fire up shell_plus:

$ python manage.py shell_plus
# Shell Plus Model Imports
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from notes.models import Note, Tag
# Shell Plus Django Imports
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.conf import settings
from django.db import transaction
from django.db.models import Avg, Count, F, Max, Min, Sum, Q
from django.utils import timezone
Python 2.7.6 (default, Sep 9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

Let's find that User we created. You'll notice that shell_plus has automatically imported something called User from django.contrib.auth.models. Thanks shell_plus!

To look for things in our database, we'll use Django's built-in ORM. This means that instead of writing unwieldy and possibly insecure raw SQL queries, we'll be able to manipulate data in a more "pythonic" way. The ORM documentation is extensive and clear, so we won't spend a lot of time talking about how to work with it here.

We know our model is called User, so let's do a query for all users in the database.

>>> User.objects.all()
[<User: root>]

Looks like there's one user in the database so far, and it's the superuser named "root" we created earlier. Since we used .all() we've returned a queryset with a single item inside. We could simply access index location zero in this queryset to get at the model object, but let's practice using .get() to return a single object.

>>> User.objects.get(username='root')
<User: root>

Nice - no brackets, so now we've got the single object itself rather than a queryset that requires iterating or indexing. Let's assign that to a variable then poke at it to see what's inside.

>>> u = User.objects.get(username='root')
>>> u.id
1
>>> u.first_name
u''
>>> u.email
u'email@email.com'
>>> u.is_superuser
True
>>> u.password
#crazy character string here

Everything seems to be in order. Django is even securely storing our password for us in a salted hash, which means we can't access the plaintext password for any of our users. This is a Good Thing when it comes to security, and we'll explore built-in methods for resetting passwords that eliminate any headache this may cause later on.

Now quit() the shell, and let's go back to our core views.py. Right now, this is the view that displays our homepage:

def home_view(request):
return render(request, 'home.html')

Let's keep things compact and log in right from the homepage. We'll need to modify our view to be able to process a POST request and log us in if we provide the proper credentials:

from django.shortcuts import render
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.http import HttpResponseRedirect

def home_view(request):

if request.method == 'POST':
username = request.POST.get('username', None)
password = request.POST.get('password', None)

auth = authenticate(username=username, password=password)
if auth is not None:
login(request, auth)
return HttpResponseRedirect(reverse('notes:index'))
else:
messages.add_message(request, messages.INFO, "Authentication Failed!")
return HttpResponseRedirect(reverse('home'))

return render(request, 'home.html')

Let's examine what this does, one chunk at a time. First, we look at the "method" of the request being made - is it a GET or POST request? If it's a GET request, nothing in the if block is executed and we fall straight to return render(request, 'home.html') at the bottom. Notice that this return statement isn't placed inside an else: block - there is a very lazy reason for this which we'll get to a bit later on.

If we instead have a POST request, we try to authenticate with the credentials supplied therein. Django makes this easy with the built-in authenticate functionality at django.contrib.auth. We simply pass the username and password as named arguments to this function and store the result in a variable called auth. If authentication fails, then auth == None. Otherwise, if auth is not None then we know that authentication was successful.

In the event of a successful authentication, we take our auth variable and pass both it and the request object to the login function also found at django.contrib.auth. With our user successfully authenticated and logged in, we direct the user over to the index view of our notes app (which we haven't written yet). If authentication fails, we attach a message to that effect and send the user back to the home/login page.

Not too bad! Let's go to home.html and whip up a basic login form. We'll edit the html file like so:

{% extends 'base.html' %}
{% load static %}
{% block body %}
<div class="jumbotron">
<h1 align='center'>DjangoNote!</h1>
<div class='container' style='padding-top:10px'>
<form action="" method="post">{% csrf_token %}
<div class="row">
<div class="col-md-6">
<input type='text' id='id_username' name='username' class='form-control' placeholder='Username' />
</div>
<div class='col-md-6'>
<input type='password' id='id_password' name='password' class='form-control' placeholder='Password' />
</div>
</div>
<br>
<input type="submit" value="Login" class='btn btn-primary btn-lg btn-block' />
</form>
</div>
</div>
{% endblock body %}

(We're making things functional here, not painting the Sistine Chapel - please forgive the inline css and the helpful line break tag. I mean, look at the blog you're on.)

Lots of cool stuff happening here. Remember that we're using the basic Bootstrap theme as our css foundation - things like col-md-6 are part of Bootstrap's grid system and make it easy to lay out our pages. You've got 12 spots in a given row to work with, so if you want two things side by side you can put them each in a column of width 6 then wrap them both in a div with class="row". We've done that here with our username and password fields. We also gave the actual input fields class="form-control", another Bootstrap built-in that makes things somewhat more pleasant to look at.

Take note as well that we included our {% csrf_token %} just after our opening form tag. CSRF tokens help prevent cross-site request forgery attacks, and Django is configured to expect them automatically when processing forms. If you forget the token, you'll get a helpful error message reminding you to include it.

Alright, let's take this baby for a spin! Point to 127.0.0.1 and you should see your DjangoNote! homepage with a brand new login form waiting for you. Try hitting the button without putting anything into the fields. You should see a message at the top of the window from the Django messaging framework telling you "Authentication Failed!" The same thing happens if we supply incorrect login credentials. If we enter our correct login information, we're presented with an error message. That's okay! We haven't written our url patterns for our notes app yet, nor have we written any views for that app. When we check the traceback, we see what line of code triggered the error:

return HttpResponseRedirect(reverse('notes:index'))

If you expand the traceback you can see the full context of where that code was executed. You probably also remember this line from our homepage / login view - this is the response returned after a successful login. Things are working! Let's write a URL pattern and a view for our Notes app so we've got something to look at once we log in. Open up notes/urls.py and create the base pattern for this app - the one that'll match 127.0.0.1/notes. We'll use this view to display an index of all of our current notes, so calling it "index" sounds good.

from django.conf.urls import patterns, include, url
from django.contrib import admin
from notes.views import index_view

urlpatterns = patterns('',
url(r'^$', index_view, name='index'),
)

Now let's go into notes/views.py and write that index_view. We want to display all of our notes on this page, so we'll need to import the Note model from notes/models.py. We'll grab the Tag model while we're at it - it'll come in handy later.

from django.shortcuts import render
from notes.models import Note, Tag

def index_view(request):
notes = Note.objects.all()
return render(request, 'notes/index.html', {'notes':notes})

We used the Django ORM to build a queryset called "notes" filled with all notes in the database. Then we passed an extra argument to our render function - a context dictionary. Now when we work with our notes/index.html template, we'll have access to that queryset through the template tag {{notes}}.

You'll also notice that we're "nesting" our templates for the Notes app in their own directory within the main templates directory. This again is for portability and sanity - I highly recommend it. Make a notes directory inside the main templates directory, and create your index.html file inside. Then, open up that index.html in your editor and make some magic:

{% extends 'base.html' %}
{% load static %}
{% block body %}

<div class="jumbotron">
<h1 align='center'>Notes!</h1>
</div>

<div class='col-md-12'>
{% if notes %}
{% for note in notes %}
<h3 class="page-header">{{note.label}}</h3>
{{note.body|linebreaks}}
{% for tag in note.tags.all %}
{{tag}}{% if not forloop.last %} | {% endif %}
{% endfor %}
{% endfor %}
{% else %}
<div class='alert alert-warning' align='center'>
<h3>No Notes Yet!</h3>
</div>
{% endif %}
</div>

{% endblock body %}

Several cool things to note here:


  1. This template extends 'base.html' even though they're in different directories. Django is smart!

  2. We've an {% if %} {% else %} setup to display one type of content if we have notes to show, and another if we don't. Remember that we passed "notes" to this template through the context dictionary back in our view. We built it by querying the database for all notes - and since we don't have any notes yet, the "notes" variable in this template is just an empty queryset. Helpfully, an empty queryset evaluates to False in this sort of usage. {% if notes %} is true if we have any notes and is false if we don't.

  3. We use a {% for %} loop to iterate through all of our notes - and we nest another {% for %} loop to iterate through the tags for any given note. You can nest as many loops as you like; just make sure to {% endfor %} for each one. We want to display the tags for any given message in the format "tag | tag | tag | tag | tag" - essentially, we want to display a space, a "|" and another space after every tag except the last one. Django makes this easy with {{forloop.last}} - as you can see, we're adding " | " after every tag so long as it isn't the last tag in the loop.

Let's step back and recall our goals for this app:


  1. See all of our existing notes

  2. Add a new note

  3. Edit and delete existing notes

  4. See all of our existing tags

  5. Create new tags

  6. Edit and delete existing tags

  7. Some basic search capability - find notes with a given tag, for instance

One down! We're well on the way to becoming Djangonauts. In part five, we're going to pick up the pace and watch our app really take start to take shape.

Continue to Part 5


<< back to blog index