Django Tutorial 3

From 118wiki

Jump to: navigation, search

Contents

Django Tutorial 3

This is a requested tutorial on how to have users log in or just have each user experience a customized content using a Django application. First, I show an ugly, insecure, "quick and dirty" way to do this using elementary URL matching; then I show the Django authorization technique.

Quick and Dirty

Here's the idea. Suppose we have a Django application with three pages, inventory, menu, and report. Each is invoked by a simple url with the name of the function (inventory/ for the inventory function, etc). Now, to customize each page for a user, just change the name of the url from, say inventory/ to inventory/thor/ (where thor is an example of the user name). Then change the inventory method within views.py so that it parses out the username. Do the same for menu and report. Of course, you'll need to change urls.py so that the extra string representing the username is allowed, but that's pretty easy.

You can parse the user name out of the URL manually, just

  from django.http import HttpResponse

then parse request.path (see request-response documentation for full details).

But there is a cleaner way to do this. Suppose we change the line for inventory in urls.py from

  (r'^inventory/$', 'mydemo.medicine.views.inventory'),

which matches only "inventory/" to instead

  (r'^inventory/(?P<username>\w+)/$', \
       'mydemo.medicine.views.inventory'),

Here, I'm using the power of regular expressions (documented here, but better explained in just about any Python book). This new url specification matches any url of the form /inventory/someusername/ (the "\w+" denotes a string of alpha characters), and also names the part of the string following /inventory/ as username. The reason this is useful is that we can now write the inventory method in views.py starting with

  def inventory(request,username=None):  

and Django will substitute the matching string for "None" in the value of username, when the inventory method is invoked -- no parsing needed inside your function! Of course you are still responsible for ensuring that the username is meaningful to your application, but that's another matter. If you want the user to "log in" or something like that, you can have another dologin.html template and a dologin method within views.py, and you can also use the HTTP redirect to replace the URL that a user first types with one that contains the user name (redirects are shown below, in the other part of this tutorial).

Using Django Authorization

This is an optional tutorial on adding user authentication to an application. This tutorial was constructed by following the Django documentation on authentication, on sessions, and on request-response objects (particularly how to do redirects). There is only one real trick -- how to set user passwords -- that is not clear from the documentation.

Source Files

The source files for the tutorial are in /group/class/c118/usertutor (you can list this directory to see the files and copy them).

Start-off

Create a new project for this,

  django-admin.py startproject userdemo

Then do:

 
  > cd userdemo
  > python manage.py startapp medicine
  > cp /group/class/c118/settings.py .
  > cp /group/class/c118/urls.py .
  > cp /group/class/c118/models.py medicine/
  > cp /group/class/c118/views.py medicine/
  > mkdir templates
  > cp /group/class/c118/*.html templates/
  > python manage.py syncdb

and assign the superuser password as requested. You may wish to look at models.py, which really only differs from Dango Tutorial 1 in that Bottle now has an owner. The main difference is in views.py, which now looks like this:

from django.shortcuts import render_to_response
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.__init__ import login
from django.contrib.auth import authenticate
from userdemo.medicine.models import Bottle, Pill
#
def inventory(request):
  if not request.user.is_authenticated():
      return HttpResponseRedirect("/login/")
  # now user is authenticated, and has name == request.user.username 
  bl = []
  for e in Bottle.objects.all():
     if e.owner != request.user.username: continue
     a = {}
     a['bottle_label'] = str(e)
     a['pill_count'] = e.pill_set.count()
     bl.append(a)
  #
  if bl != []: 
     return render_to_response('inventory.html', { 'bl': bl } )
  else: 
     return HttpResponse("<html><h2>%s: you have no bottles to display.</h2></html>" % request.user.username)

def prelogin(request): 
  return render_to_response('login.html')

def logintry(request):
    username = request.POST['username']
    password = request.POST['password']
    user = authenticate(username=username, password=password)
    if user is not None:
        if user.is_active:
            login(request,user)
            # Redirect to a success page,
            # which normally would be a menu with many links, but
            # here, we only have inventory
            return HttpResponseRedirect("/inventory/")
        else:
            # Return a 'disabled account' error message
            # or, if lazy, just go back to login ...
            return HttpResponseRedirect("/login/")
    else:
        # Return an 'invalid login' error message.
        # or, if lazy like me, just make them try again
        return HttpResponseRedirect("/login/")

You'll notice there are more import statements to get various authentication, login, and HttpResponse functionality. Also, notice that the inventory method forces the invoker to be authenticated; authentication is handled by a login.html page, which contains a form (find the source for that page in templates). The prelogin() method is invoked when there is a request for "/login/" (as controlled via urls.py), whereas the logintry() method is invoked for "/logintry/" (again, see urls.py). The request POST dictionary accessed by logintry() is established by the login.html form, which uses the HTTP POST method and names its fields (username, password) as expected.

Testing

As usual, start the server

 > python manage.py runserver 0.0.0.0:8910

and use a browser to communicate with the server. Initially, there are no bottles or pills. But before you create any bottles or pills, create a user. When you create a user, you can assign name, email, password, and other attributes. However, the password field is deceptive here -- not what you want. So just fill in anything. Suppose you create a user named oliver. Save the new user and stop the server (Ctrl-C). Now, to give oliver a real password, follow this procedure:

  > python manage.py shell
  >>> from django.contrib.auth.models import User
  >>> u = User.objects.get(username__exact='oliver')
  >>> u.set_password('foo')
  >>> u.save()
  Ctrl-D

Explanation: the first command starts up Django as an interactive shell rather than a server; the "from .." statement exposes the model for users (the table of all users in the database). The "u = .." statement is a query to find the user named oliver; the "u.set..." gives user oliver a new password (foo). Then, to commit this update to the database, the "u.save()" line is needed.

Now you can restart the server

 > python manage.py runserver 0.0.0.0:8910

perhaps add other users, add bottles and specify owners (owners are just the users that you have added). Finally, log out. Then try

 http://l-lnx119.divms.uiowa.edu:8910/inventory/

and you should see a login screen. Log in as oliver, and you will see the bottles owned by oliver.

Personal tools