Milestone 4

due at 23:59 on Sun 4 Mar

Again, the following commands will update your repository to the latest code that I may have added:

liucs:~$ cd cs164/bookswap
liucs:~/cs164/bookswap$ git pull

Before continuing, make sure you are caught up on Milestone 3.

If you got stuck on M3:

Here is how you can abandon your changes and continue your project using mine. Do this only if you feel you need it to make progress, or if I advise you to! After finishing this sequence of commands, send me an email to let me know how it went (or report anything that looks like an error).

## Use these commands ONLY if you are stuck on M3
liucs:~/cs164/bookswap$ git commit -am "unsaved changes"
liucs:~/cs164/bookswap$ git merge -X theirs origin/solution
liucs:~/cs164/bookswap$ git push
liucs:~/cs164/bookswap$ rm -f dbfile
liucs:~/cs164/bookswap$ python manage.py syncdb --noinput
liucs:~/cs164/bookswap$ python manage.py loaddata sample

After adding the sample data, your admin interface username is admin and the password is admin.

(A) Preliminaries

Templates directory

To use templates, we have to specify a template directory in the settings.py file. Create the directory in the shell:

liucs:~/cs164/bookswap$ mkdir templates

and then find the section for TEMPLATE_DIRS in settings.py (around line 105) and make it look like this:

from os.path import dirname, join
CWD = dirname(__file__)  # double underscores
TEMPLATE_DIRS = (
    join(CWD, 'templates'),
    # Put strings here, like "/home/html/django_templates"
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

This code automatically creates an absolute path to your templates directory, by substituting in the directory of the settings.py file itself.

Using a template

Now let’s try the simplest possible template. In urls.py, add an entry like this:

    url(r'^template-test/', 'app.views.template_test'),

If you load http://localhost:8000/template-test/ in your browser on the VM (of course, you need to have runserver working, as usual), you should see an error ViewDoesNotExist.

In app/views.py, add these imports at the top:

from django.shortcuts import render_to_response
from django.template import RequestContext
from django.db.models import Q

and this simple view function:

def template_test(request):
    return render_to_response('test.html', {'a': 42})

Reload the template-test URL and you should see the error message change to “Template not found.”

Finally, create a new file test.html in your templates directory, with these contents:

<h1>Test</h1>
The value of A is {{a}}.

Reload the template-test URL and it should say “The value of A is 42.”

Template test page

Template test page

(B) Search results

As discussed in class, we’ll save the search form for later, but we can still do the search results. The URL will look like this:

http://localhost:8000/search?isbn=1234&title=Photo&authors=Chris

and so on for other search fields. The rule here is that, after the question mark, you have multiple key=value pairs, separated by &.

Initial search view

Add the following to the URL configuration:

    url(r'^search', 'app.views.search'),

Start your function in app/views.py:

def search(request):
    return render_to_response('search.html')

And then create an initial template in templates/search.html:

<h1>Search Results</h1>

Verify that all that works and displays the “Search Results” header:

Search results header

Search results header

Search by ISBN

Now we’ll handle the various search fields, one at a time. Let’s start with isbn. We’ll use a filter along with a so-called “Q” object (explained below), and pass the results to the template.

def search(request):
    results = Book.objects.filter(Q(isbn=request.GET['isbn']))
    return render_to_response('search.html', {'results': results})

The template should be modified accordingly:

<h1>Search Results</h1>
<ul>
{% for r in results %}
<li>{{r.title}} by {{r.authors}} ({{r.isbn}})
</li>
{% endfor %}
</ul>

Set isbn= to an actual ISBN from your sample data, and you’ll get that result back:

Search by ISBN

Search by ISBN

Search by title, and the Q object

Okay, now about that Q notation. This gives us a very flexible way to combine search queries, including Boolean and (&), or (|), not (~). So if I want to find books matching the ISBN or the title, it looks like this:

    results = Book.objects.filter(
        Q(isbn = request.GET['isbn']) |
        Q(title__contains = request.GET['title']))

Here’s a sample result using this code:

Search by title and ISBN

Search by title and ISBN

We get the Chemistry book because it is ISBN 3888472910, and the other two because the title contains “Photo”.

Et cetera

Try adding some other search fields, such as authors. Should this also be combined with the others using or (|), or would the Boolean and make more sense?

Next, try searching by course, including the prefix, number, and title. This is a little more complex. You can still add them to the Q object, like this:

    results = Book.objects.filter(
        Q(isbn=request.GET['isbn']) |
        Q(title__contains=request.GET['title']) |
        Q(course__prefix=request.GET['prefix']))

However, because course is a ManyToManyField, we end up getting back several matches:

Search using course prefix, multiple matches

Search using course prefix, multiple matches

This is easy to fix by adding .distinct() to the end of the filter function:

    results = Book.objects.filter(
        Q(isbn=request.GET['isbn']) |
        Q(title__contains=request.GET['title']) |
        Q(course__prefix=request.GET['prefix'])).distinct()

If you can get authors to work but are having too much trouble with course, it’s fine to move on.

(C) Book page

Next we’ll do a page that displays detailed information about one book, including all the buy/sell listings for that book.

Initial version

If you don’t already have it, add this to your URL conf:

    url(r'^book/(\d+)/', 'app.views.showbook'),

As described in class, the parentheses in the URL pattern indicate there is a parameter there. The \d matches any digit (0–9), and because there is a + after it, that means one or more digits. So the URL will match /book/1/ or /book/932/, but not /book/9a/. For this parameter, we’ll use the id field automatically assigned to every book, rather than the ISBN.

In app/views.py, your showbook function will have an extra parameter that is supplied by the number from the URL. Here’s an initial version:

def showbook(request, id):
    b = Book.objects.get(id=id)
    return HttpResponse(
        "ISBN: %s<br> Title: %s<br> Authors: %s<br> Publisher: %s<br>" %
        (b.isbn, b.title, b.authors, b.publisher))

You can test that and see one of your books:

Template version

Using what you learned about templates and render_to_response, create a book.html template that displays the same information after you change showbook as follows:

def showbook(request, id):
    b = Book.objects.get(id=id)
    return render_to_response('book.html', {'book': b})

You may also want to play with the formatting in the HTML template. Use <h1> to make the title a heading, for example.

More data

Now, we want to list buyers and sellers on the template page. You will:

Try to make it look something like this:

How did I get the count of people in each category? For any query result, you can just add .count (if it’s in a template), or call .count() (if it’s in Python code).

<h2>{{buyers.count}} people want to buy</h2>

Notice that, in the screen shot, it says “1 people want to sell,” which is a little awkward. You can fix stuff like that with the pluralize filter. It takes a number on the left side, and a string with two alternatives (singular, then plural) on the right:

<h2>{{sellers.count}}
    {{sellers.count|pluralize:"person wants,people want"}}
    to sell
</h2>

Now if sellers.count is 1, it says “1 person wants to sell.” Otherwise it will use “N people want to sell.”

We want the search results page to link to the Here’s the syntax for an HTML hyperlink:

<a href="/book/{{r.id}}/">{{r.title}}</a>

Verify that it connects you from the search results to the appropriate book page.

(D) User profile page

For this one, you’re more on your own. We want the user names in the selling and listing pages to link to a user profile page containing their contact information. For now, that probably just means their user name and email address. Later on we’ll look at how to add a more detailed user profile, which could contain phone numbers, preferences, etc.

The fields of the User object are called

Make sure that the links on the book page take you to the correct user profile:

Submit

That’s it, but feel free to embellish! Ask questions by email or on the forum about anything fancier you’d like to try.

To submit, first try git status and look for any untracked files. These are files you created that did not exist before, such as your templates.

liucs:~/cs164/bookswap$ git status
# On branch master
# Your branch is ahead of 'league/master' by 4 commits.
#
# Changes not staged for commit:
#   (use "git add ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes)
#
#	modified:   app/views.py
#	modified:   urls.py
#
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
#	templates/book.html
#	templates/user.html
no changes added to commit (use "git add" and/or "git commit -a")

Before committing, you will have to add all of these new files:

liucs:~/cs164/bookswap$ git add templates/*.html

Finally, you can do the usual two-step commit and push:

liucs:~/cs164/bookswap$ git commit -am "my milestone 4"
liucs:~/cs164/bookswap$ git push
comments powered by Disqus

 

©2012 Christopher League · some rights reserved · CC by-sa