Django for Designers

From OpenHatch wiki
Revision as of 04:45, 12 March 2013 by imported>Paulproteus (→‎Part 3: Models, our database, and making it visible: Moved to a new page)

Introduction

In this tutorial, we will explain topics and provide commands for you to run on your own computer. You will leave with a working social bookmarking web app!

This is a tutorial on web programming, so we will go beyond just Django and discuss third-party Django apps and other real-world web development tools. We'll also be emphasizing areas of Django that particularly affect designers, such as static files, template inheritance, and AJAX.

Things you should know already

  • HTML familiarity
  • Basic Python proficiency
  • Basic or better Javascript proficiency
  • Pre-requisites that we will help with

We expect you to have git, Python, and a few other elements ready on your laptop before the tutorial. We have published a laptop setup guide that steps you through:

  • Installing Python, git, pip, virtualenv, and a reasonable text editor
  • Setting up your env with Django, South, and django-debug-toolbar
  • Basic command line knowledge (cd, ls, etc)
  • Basic git knowledge
  • Setting up your git repo for the tutorial

Things you do not need to know already:

  • Django :)
  • What an ORM is
  • Anything database related

Curriculum

Part 3.5: Changing our mind and adding users

D'oh! You know what every social bookmarking app has, that ours doesn't have? Users!

I don't mean like the number of people using it--I mean a way to store different users' accounts and keep track of who owns which bookmarks. So let's change our app to include this feature.

Lucky for us, Django comes with an app for user accounts and authentication from the get-go! In fact, it's already installed. If you look back at your settings.py file, you'll see that in INSTALLED_APPS, there is an entry for 'django.contrib.auth'.

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'south',
    'bookmarks',
)

That's our authentication app!

Let's open the Django shell and play with this app a bit.

# in django-for-designers/myproject
$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> User.objects.all()
[<User: karen>]

Whaaaaat?? There's already a User here. How can that be?

You might recall making a 'superuser' account when you first set up your Django project. That superuser was, in fact, created using Django's built-in auth app.

What is our user account's id number?

>>> me = User.objects.all()[0]
>>> me.id
1

Neato!

Add user field to bookmark

Now we need to create a relationship between the built-in User model and our Bookmark model.

First, in our models.py file, we need to import django.contrib.auth's built-in User model so that we can refer to it in our models.

from django.db import models
from django.contrib.auth.models import User

Now we need to think--what kind of relationship do users and bookmarks have?

Well, a user can have multiple bookmarks. But (right now, anyway) a bookmark should only have one user. So that means that we should use a ForeignKey field to add the user to our Bookmark model.

class Bookmark(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=200, blank=True, default="")
    url = models.URLField()
    timestamp = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return self.url

While we're at it, let's update the __unicode__ method too to let us know to whom a bookmark belongs to.

class Bookmark(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=200, blank=True, default="")
    url = models.URLField()
    timestamp = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return "%s by %s" % (self.url, self.author.username)

Make a migration in South

Now that we've added a field to our model, we are going to need to create a database migration. South can help us do this!

To create our migration, run

# in django-for-designers/myproject
$ python manage.py schemamigration bookmarks --auto

Note that we're now using --auto instead of --initial (which we used back when we first wrote our models and set up our app to use South).

Eep! Before it will make our migration file, South wants some information from us:

 ? The field 'Bookmark.author' does not have a default specified, yet is NOT NULL.
 ? Since you are adding this field, you MUST specify a default
 ? value to use for existing rows. Would you like to:
 ?  1. Quit now, and add a default to the field in models.py
 ?  2. Specify a one-off value to use for existing columns now
 ? Please select a choice: 2

We could in theory modify our models to specify a default value for author, or make it optional. But neither of those sound like good options--we *want* future bookmarks to be forced to have an author! So we'll choose 2, to set up a default value for our new author field just for the purposes of this migration.

 ? Please enter Python code for your one-off default value.
 ? The datetime module is available, so you can do e.g. datetime.date.today()
 >>>

We then need to come up with a default value. Well, right now there's only one User in our system who the sample bookmarks we'd entered so far could belong to--our superuser account, which (if you don't remember) had an ID number of 1.

So let's enter 1 for our default.

 ? Please enter Python code for your one-off default value.
 ? The datetime module is available, so you can do e.g. datetime.date.today()
 >>> 1
 + Added field author on bookmarks.Bookmark
Created 0002_auto__add_field_bookmark_author.py. You can now apply this migration with: ./manage.py migrate bookmarks

Remember, the first step creates the migration, but doesn't run it. So let's do what South says and run a command to apply our migration!

# in django-for-designers/myproject
$ python manage.py migrate bookmarks
Running migrations for bookmarks:
 - Migrating forwards to 0002_auto__add_field_bookmark_author.
 > bookmarks:0002_auto__add_field_bookmark_author
 - Loading initial data for bookmarks.
Installed 0 object(s) from 0 fixture(s)

Save and commit your work to add users to your bookmarks app!

Templates and links for login/logout/etc

Django's auth app comes with built-in views, which we can use to handle login and logout functionality for our users. Once we point some URLs at them, that is.

First, let's edit urls.py:

urlpatterns = patterns('',
    url(r'^$', 'bookmarks.views.index', name='home'),
    url(r'^bookmarks/$', 'bookmarks.views.index', name='bookmarks_view'),
    url(r'^tags/(\w+)/$', 'bookmarks.views.tag'),
    url(r'^login/$', 'django.contrib.auth.views.login'),
)

Now if anyone goes to localhost:8000/login/, the built-in login view will get triggered.

Run your dev server and try that. What error do you see?

While there is a built-in login view, there is no built-in login template. We need to build one for it. While we could put it anywhere and tell login explicitly where to look, by default the login view expects the template to reside at templates/registration/login.html. So we may as well put it there.

# in django-for-designers/myproject
$ cd bookmarks/templates/
# in django-for-designers/myproject/bookmarks/templates
$ mkdir registration

Create a new file within the new registration directory called login.html, and put this inside:

{% extends "base.html" %}

{% block subheader %}Login{% endblock %}

{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

<form method="post" action="{% url 'django.contrib.auth.views.login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="login" />
<input type="hidden" name="next" value="/" />
</form>
{% endblock %}

Note the hidden input with the name "next". This input tells the login view what URL to send the user to after they successfully log in. We have it set to '/', so it'll just take them back to the home page.

We need to add a logout view too! Let's do that. Back in urls.py:

url(r'^login/$', 'django.contrib.auth.views.login'),
url(r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': '/'})

The dictionary after the logout URL sends some extra arguments to the logout view. Specifically it tells the logout view where to send the user after they log out. We could make a special goodbye splash page or something, but nah, let's just send them back to the home page again.

Now we need to add a login/logout link to our site so people can actually use these views! We want this link to be at the top of every page, so we'll edit our base template:

<body>
    <div id="container">
        <div id="header">
            {% block bookmark_widget %}
            {% endblock %}
            <div id="authentication">
            {% if user.is_authenticated %}
                Hi {{user}}! <a href="{% url 'django.contrib.auth.views.logout' %}">Logout</a>
            {% else %}
                <a href="{% url 'django.contrib.auth.views.login' %}">Login</a>
            {% endif %}
            </div>
            <h1><a href="/">My bookmarking app</a></h1>
        </div>
        [ ... ]

Our template checks to see if there's a logged-in user, and if so, shows a hello message and a logout link. If the user isn't logged in, it shows a login link instead.

Check http://localhost:8000 and try logging in with the superadmin username and password you created before! It should work. :)

You might be wondering--where did the 'user' variable in the template come from? If you look at views.py, you'll notice we never added a user variable to our context dictionary. So how did this happen?

The answer is in the function we are using to render our templates, render(). render() automatically uses the request we sent it to create a Django RequestContext, which contains a bunch of extra context variables that get sent along to every view that uses a RequestContext. The Django auth app adds the current user to the RequestContext automatically. This is handy, since you don't want to have to look up the current user to every single view you ever write separately, just so the nav section on your website will work everywhere!

There are other functions that return an HTMLResponse, like render(), but don't include a RequestContext. render_to_response() is one common shortcut function that doesn't include it by default; another was the HttpResponse() function we used earlier! Just something to remember--if you're trying to send a piece of data to almost every page in your web app, 1.) you probably want to use render() in your views, and 2.) you want to find a way to add your data to your app's RequestContext. (https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.RequestContext says more about this!)

Save and commit your changes in adding login/logout functionality.

Modify views and templates to use model data

Let's edit our views and templates so they use real bookmark data from our database!

Add more data

First, let's add a bit more bookmark and tag data to our database. We'll use a Python loop to add a lot of bookmarks at once.

# in django-for-designers/myproject
$ python myproject/manage.py shell

That will bring us to the Django management shell. Within that special Python prompt, type the following:

(Remember, any time you see ">>>", you don't have to type those characters.)

>>> import django.contrib.auth.models
>>> me = django.contrib.auth.models.User.objects.all()[0]

This tells Python we'll be accessing the user model, and then grabs the first user, storing it in a variable called me.

Keeping that Python prompt open, run the following commands:

>>> import bookmarks.models
>>> import random
>>> tag_names = ['kids', 'read_on_plane', 'send_to_mom']
>>> # Create the Tag objects
>>> for tag_name in tag_names:
...     tag = bookmarks.models.Tag(slug=tag_name)
...     tag.save()
... 
>>> kids_urls = ['http://pbskids.org/', 'http://www.clubpenguin.com/', 'http://www.aplusmath.com/hh/index.html', 'http://kids.nationalgeographic.com/kids/activities/recipes/lucky-smoothie/', 'http://www.handwritingforkids.com/', 'http://pinterest.com/catfrilda/origami-for-kids/', 'http://richkidsofinstagram.tumblr.com/', 'http://www.dorkly.com/picture/50768/', 'http://www.whyzz.com/what-is-paint-made-of', 'http://yahooligans.com/']
>>> for kids_url in kids_urls:
...    b = bookmarks.models.Bookmark(author=me, url=kids_url)
...    b.save()
...    how_many_tags = random.randrange(len(tag_names))
...    for tag_name in random.sample(tag_names, how_many_tags):
...        b.tag_set.add(bookmarks.models.Tag.objects.get(slug=tag_name))
...
>>>

This creates a number of bookmarks, tagged appropriately! You can exit the shell by typing:

>>> exit()
Get views.py to talk to our models

Then, we'll need to have our views.py file import the bookmark model and send data to the index view.

We'll add an import statement:

from bookmarks.models import Bookmark

And we'll edit index(request):

def index(request):
    bookmarks = Bookmark.objects.all()
    context = {
        'bookmarks': bookmarks
    }
    return render(request, 'index.html', context)

Wait! We don't actually want to load every bookmark in our database when we go to the front page. If we have lots of bookmarks, that will get slow and unwieldy quickly.

Instead, let's show the 10 most recent bookmarks:

def index(request):
    bookmarks = Bookmark.objects.all().order_by('-timestamp')[:10]
    context = {
        'bookmarks': bookmarks
    }
    return render(request, 'index.html', context)

We also want our tag view to show real bookmarks. We only want to show bookmarks that have been tagged with the given tag. So we'll edit the import statement we just added:

from bookmarks.models import Bookmark, Tag

And alter the tag() definition as well:

def tag(request, tag_name):
    tag = Tag.objects.get(slug=tag_name)
    bookmarks = tag.bookmarks.all()
    context = {
        'tag': tag,
        'bookmarks': bookmarks,
    }
    return render(request, 'tag.html', context)
Change templates to handle bookmark data

Now, let's modify our templates to use this data.

In index.html, update just block content per the following. Make sure to keep the extends directive and the block subheader directive.

{% block content %}
<ul class="bookmarks">
    {% for bookmark in bookmarks %}
    <li>
        <a class="bookmark-link" href="">{{ bookmark }}</a>
        <div class="metadata"><span class="author">Posted by Jane Smith</span> | <span class="timestamp">2012-2-29</span> | <span class="tags"><a href="">funny</a> <a href="">haha</a></span></div>
    </li>
    {% endfor %}
</ul>
{% endblock %}

Instead of writing out each bookmark list element individually ahead of time, we are using a Django template language for loop to create a list element for each bookmark. Thus, we only have to specify the HTML formatting of our list elements once!

Start your development server and look at http://localhost:8000 now. You'll see that instead of the fake HTML we had before, each link's text is the unicode representation of each bookmark in your database. Cool, huh?

That's okay, but the unicode representation isn't really what we want to go there. Let's fix that:

    <a class="bookmark-link" href="{{ bookmark.url }}">
    {% if bookmark.title %}{{ bookmark.title }}{% else %}{{ bookmark.url }}{% endif %}
     </a>

If the bookmark has a title, we'll make the title the link. Otherwise we'll show the URL. We also fill in the URL on the <a> tag, to make the link work.

We also want to fill in the other metadata, like tags and author info. To do that. (FIXME: Where do people make this change?)

<div class="metadata"><span class="author">Posted by {{ bookmark.author }}</span> | <span class="timestamp">{{ bookmark.timestamp }}</span>
        {% if bookmark.tag_set.all %}| <span class="tags">
            {% for tag in bookmark.tag_set.all %}
                <a href="{% url 'bookmarks.views.tag' tag.slug %}">{{ tag.slug }}</a></span>
            {% endfor %}
        {% endif %}
</div>

We check if there are any tags for the bookmark, and if so loop over the tags to put each of them in.

Additionally, we use Django's {% url %} tag to generate the URL for our tag's link. The URL tag takes first an argument which is the path of a particular view function (bookmarks.views.tag), then additional arguments for any input variables that the function expects to glean from the URL. (As you may recall, the tag view takes an argument tag_name, which is the name of the tag in question).

Why use {% url %} instead of just writing "/tags/{⁠{ tag.slug }}"? Django principle of DRY--Don't Repeat Yourself--means that we want to avoid duplicating work as much as possible. If later down the line we decided to change our URL structure so that tag pages would appear at "/bookmarks/by_tag/<tag_name>" instead, we'd have to go in and fix all these hard-coded URL patterns by hand. Using the {% url %} tag makes Django generate our URL for us, based on our urls.py file, so any changes we make automatically get propagated outward!

Run

# in django-for-designers/myproject
$ python manage.py runserver

and confirm that your tag links working and our other changes are visible. It's easy to typo or make a mistake with {% url %} tags, so we want to make sure everything is working.

The default timestamp formatting that Django gave us is pretty neat, but in our original mockup we just used the date--not this long timestamp. Fortunately, Django's built-in filters make it easy to format a date or time any way we want! Since we just want to show the date, we'll use the date filter:

<span class="timestamp">{{ bookmark.timestamp|date:"Y-m-d" }}</span>

We tell Django that we're using a filter via the |filter_name syntax. The arguments that come after the colon are a standard Python code for describing different ways of formatting dates. You can read more about the date filter and all the different formatting codes at https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date. For our purposes, Y outputs the full four-digit year, while m and d outputs the month and the day as two digit numbers. The hyphens we put between them are included in the formatting, too -- if we wanted the date to use slashes instead, we'd simply write |date:"Y/m/d".

Spin up your dev server, if you haven't got it running already, and check out your changes! Then save and commit your precious work.

Dealing with errors

Let's update our tag.html template to use the same formatting as index.html. Since we're using the same formatting for the list of bookmarks, and there aren't any other content block differences between index.html and tag.html, we can simply modify our tag template to inherit from index.html. Then the only block we need to overwrite is the subheader:

{% extends 'index.html' %}

{% block subheader %}Bookmarks tagged {{ tag }}{% endblock %}

Now we can go to http://localhost:8000/tags/funny and see a nicely styled list of bookmarks tagged with that tag!

What happens if we go to http://localhost:8000/tags/asdfjkl?

Eek, a DoesNotExist error! That's not so great. Our application is erroring on one of our view lines:

tag = Tag.objects.get(slug=tag_name)

Fortunately, Django has a shortcut function that can help us -- get_object_or_404(). This function will attempt to get a Django model based on the parameters you give it, and if it fails, automatically throw an HTML 404 error.

from django.shortcuts import render, get_object_or_404
from bookmarks.models import Bookmark, Tag


[...]


def tag(request, tag_name):
    tag = get_object_or_404(Tag, slug=tag_name)
    bookmarks = tag.bookmarks.all()
    context = {
        'tag': tag,
        'bookmarks': bookmarks,
    }
    return render(request, 'tag.html', context)

Now http://localhost:8000/tags/asdfjkl will throw a nicer 404 error. We could even make a pretty 404.html template to handle such errors! There are also shortcuts in Django for 500 and 403 (Forbidden) errors, if you want to handle and style those as well.

Save and commit your error-catching work.

Let me show off

So far, we've all only been able to access our own Django-powered sites. In this section, you will see how to access my (the instructor's) super cool bookmarks site! (We'll talk later about how you can share your code the same way. It might require firewall configuration changes on your computer, so we save that complexity for later.)

Assuming that you and the instructor are on the same wifi network, you can visit a temporary website on the instructor's computer. Visit this link:

http://192.168.1.1:8000/

(If you are wondering exactly who can connect to the instructor's app, it is typically only people on the same wifi/wired network as her, rather than the whole Internet. This is due to the a technique in wide use called "network address translation", and it is not considered perfect security.)

When you visit the instructor's app, you are interacting with the database stored on her laptop. So all the changes made by other people in the room are reflected in what you see! So try not to be too obscene.