Thinking in GIS
a blog about GIS from a urban geogeek living at the countryside
The Pinax Tutorial #2: Developing the basic application and plugging it in Pinax
Posted: November 29, 2009Categories: Python, Django, devs, The Pinax Tutorial, Pinax, Tutorials, Uncategorized
Feedback: View Comments
In this part of the tutorial you are going to create the core of the bookstore application, with all the pages that gives access to the CRUD (Create, Read, Update, Delete) features. And you are goint to plugin this basic application into Pinax.
After finishing with this part you will have the core of the bookstore application working as desired. You will be able to:
- see a list with all the books in the bookstore
- add a new book
- update and delete your books
- see a list of all books added by you
- see a list of all books added by a user
In the following parts you will add some more goodness like localization, pagination, avatars, profiles, voting, tagging, feeds, comments, notifications, and flags for contents, but basically this part is the most important one and it is the way you can create from scratch an application and make it enabled for Pinax.
Creating the application
Before doing anything, give execute permissions to the manage.py command:
chmod 777 manage.py
Now create the bookstore application:
./manage.py startapp bookstore
Now that you have created the application, it it time to add the models, installing it in the Pinax project and syncing the database.
Creating the models
For creating the models you need to edit the models.py that has been created for you when creating the application. Our application will be composed by a single model, named Book.
PROJECT_ROOT/bookstore/models.py
# python import os from os import path from datetime import datetime# django imports from django.conf import settings from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _
class Book(models.Model): """ Book Model: title, publisher, author, description, coverart, adder, added """ title = models.CharField(('title'), max_length=255) publisher = models.CharField(('publisher'), max_length=255) author = models.CharField(('author'), max_length=255) description = models.TextField(('description'), blank=True) coverart = models.ImageField(upload_to="bookstore", blank=True, null=True) adder = models.ForeignKey(User, related_name="added_books", verbose_name=('adder')) added = models.DateTimeField(('added'), default=datetime.now)
<span class="k">def</span> <span class="nf">get_absolute_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="s">"describe_book"</span><span class="p">,</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">pk</span><span class="p">])</span>
<span class="n">get_absolute_url</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">permalink</span><span class="p">(</span><span class="n">get_absolute_url</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">__unicode__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">ordering</span> <span class="o">=</span> <span class="p">(</span><span class="s">'-added'</span><span class="p">,</span> <span class="p">)</span>
<span class="k">def</span> <span class="nf">_get_thumb_url</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">folder</span><span class="p">,</span> <span class="n">size</span><span class="p">):</span>
<span class="sd">""" get a thumbnail giver a folder and a size. """</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="p">:</span>
<span class="k">return</span> <span class="s">'#'</span>
<span class="n">upload_to</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
<span class="n">tiny</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">upload_to</span><span class="p">,</span> <span class="n">folder</span><span class="p">,</span> <span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="o">.</span><span class="n">path</span><span class="p">))</span>
<span class="n">tiny</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">normpath</span><span class="p">(</span><span class="n">tiny</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">tiny</span><span class="p">):</span>
<span class="kn">import</span> <span class="nn">Image</span>
<span class="n">im</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
<span class="n">im</span><span class="o">.</span><span class="n">thumbnail</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">Image</span><span class="o">.</span><span class="n">ANTIALIAS</span><span class="p">)</span>
<span class="n">im</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">tiny</span><span class="p">,</span> <span class="s">'JPEG'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="o">.</span><span class="n">url</span><span class="p">),</span> <span class="n">folder</span><span class="p">,</span> <span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="o">.</span><span class="n">path</span><span class="p">))</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'</span><span class="se">\'</span><span class="s">, '</span><span class="o">/</span><span class="s">')</span>
<span class="k">def</span> <span class="nf">get_thumb_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_thumb_url</span><span class="p">(</span><span class="s">'thumb_100_100'</span><span class="p">,</span> <span class="p">(</span><span class="mi">100</span><span class="p">,</span><span class="mi">100</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">thumb</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">""" Get thumb <a>. """</span>
<span class="n">link</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_thumb_url</span><span class="p">()</span>
<span class="k">if</span> <span class="n">link</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">return</span> <span class="s">'<a href="#" target="_blank">NO IMAGE</a>'</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="s">'<img src=</span><span class="si">%s</span><span class="s"> />'</span> <span class="o">%</span> <span class="p">(</span><span class="n">link</span><span class="p">)</span>
<span class="n">thumb</span><span class="o">.</span><span class="n">allow_tags</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">def</span> <span class="nf">fullpicture</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">""" Get full picture <a>. """</span>
<span class="n">link</span> <span class="o">=</span> <span class="s">"</span><span class="si">%s%s</span><span class="s">"</span> <span class="o">%</span> <span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">MEDIA_URL</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">coverart</span><span class="p">)</span>
<span class="k">if</span> <span class="n">link</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">return</span> <span class="s">'<a href="#" target="_blank">NO IMAGE</a>'</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="s">'<img src=</span><span class="si">%s</span><span class="s"> />'</span> <span class="o">%</span> <span class="p">(</span><span class="n">link</span><span class="p">)</span>
<span class="n">thumb</span><span class="o">.</span><span class="n">allow_tags</span> <span class="o">=</span> <span class="bp">True</span>
A few notes about the Book's model:
- The basic application of the tutorial implements these fields: title, publisher, author, description, coverart, adder, added
- Also an identifier field, named id, will be created by Django behind the scenes
- The adder field is a foreign key to the user table. This is why you need to import User from Django.contrib.auth.models
- The default ordering will be based on the time each book has been added, from the newest to the oldest
- The thumb method returns the url of the thumbnail of the coverart
- The fullpicture method returns the url of the coverart
If you wish that the bookstore application may be managed also from the Django Admin application (this is mainly useful for letting the system administrator to interact with the system), create an admin.py file like this one:
PROJECT_ROOT/bookstore/admin.py
from django.contrib import admin from bookstore.models import Bookclass BookAdmin(admin.ModelAdmin): list_display = ('title', 'publisher', 'description', 'added', 'adder', 'coverart')
admin.site.register(Book, BookAdmin)
A note about localization
One of the aims of this tutorial is also to make the developer confident with the powerful Django's localization features. One of the part of the tutorial will teach you how to develop a localization-ready application.
Meanwhile just be aware that we will make the strings of the application easily translatable by using two mechanism:
- in the models (as you may have noticed) you will use the ugettext_lazy class from django.utils.translation
- in the templates you will use the {% blocktrans %} block
Installing the application in the Pinax project
Add the Bookstore application in the settings.py file. Do the same with the Admin application if you want to manage the bookstore application also from there:
PROJECT_ROOT/settings.py
INSTALLED_APPS = ( # included 'django.contrib.auth', 'django.contrib.contenttypes', ...
<span class="c"># external</span>
<span class="s">'notification'</span><span class="p">,</span> <span class="c"># must be first</span>
<span class="s">'django_openid'</span><span class="p">,</span>
<span class="s">'emailconfirmation'</span><span class="p">,</span>
<span class="o">...</span>
<span class="c"># internal (for now)</span>
<span class="s">'analytics'</span><span class="p">,</span>
<span class="s">'profiles'</span><span class="p">,</span>
<span class="s">'account'</span><span class="p">,</span>
<span class="o">...</span>
<span class="s">'django.contrib.admin'</span><span class="p">,</span> <span class="c"># add this line</span>
<span class="c"># my apps</span>
<span class="s">'bookstore'</span><span class="p">,</span> <span class="c"># also add this line</span>
)
Syncronizing the database
Now that you created a new model, you need to syncronize the database:
./manage.py syncdb
Note that only a new table is generated, and its named bookstore_book. This is the SQL that has been ran by syncdb:
CREATE TABLE "bookstore_book" ( "id" integer NOT NULL PRIMARY KEY, "title" varchar(255) NOT NULL, "publisher" varchar(255) NOT NULL, "author" varchar(255) NOT NULL, "description" text NOT NULL, "coverart" varchar(100), "adder_id" integer NOT NULL REFERENCES "auth_user" ("id"), "added" datetime NOT NULL )
Consider that I am using Sqlite, if you are using a different database like Postgres, MySQL... the SQL may be slightly different.
Time to run the server and test your new application:
./manage.py runserver
Now if you access the admin interface, at: http://localhost:8000/admin you will be able to manage your bookstore with basic CRUD pages.
To integrate this CRUD pages into Pinax, you are going to write the views of these pages in the next sections, so keep going with the tutorial.
Configuring the urls
Configuring the urls means to design how to map urls to pages. Any web developers with good habits should plan the pages composing the application that is developing, and mapping them to corresponding urls.
For the Bookstore application this is what is planned to be:
- A page (the home page of the bookstore application) for getting a list of all books: http://localhost:8000/bookstore/
- A page for getting a list of your books: http://localhost:8000/bookstore/your_books/
- A page for getting a list of the books added by a different users: http://localhost:8000/bookstore/user_books/an_user/
- A page for adding a book: http://localhost:8000/bookstore/add/
- A page for viewing a book, given its identifier: http://localhost:8000/bookstore/4/book/ (it this case the book with id=4)
- A page for updating a book, given its identifier: http://localhost:8000/bookstore/4/update/ (it this case the book with id=4)
Now that we planned the urls needed by the bookstore application, you add the mappings to the views in the urls.py file. Instead than adding these mappings to the central urls.py file of the project, for clearity you create a urls.py file in the bookstore application directory. Now you need to include this directory in the project urls.py file.
This is where to add the file in the urls.py project file:
PROJECT_ROOT/urls.py
urlpatterns = patterns('',
<span class="o">...</span>
<span class="p">(</span><span class="s">r'^feeds/tweets/(.*)/$'</span><span class="p">,</span> <span class="s">'django.contrib.syndication.views.feed'</span><span class="p">,</span> <span class="n">tweets_feed_dict</span><span class="p">),</span>
<span class="p">(</span><span class="s">r'^feeds/posts/(.*)/$'</span><span class="p">,</span> <span class="s">'django.contrib.syndication.views.feed'</span><span class="p">,</span> <span class="n">blogs_feed_dict</span><span class="p">),</span>
<span class="p">(</span><span class="s">r'^feeds/bookmarks/(.*)/?$'</span><span class="p">,</span> <span class="s">'django.contrib.syndication.views.feed'</span><span class="p">,</span> <span class="n">bookmarks_feed_dict</span><span class="p">),</span>
<span class="c"># bookstore urls.py file</span>
<span class="p">(</span><span class="s">r'^bookstore/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="s">'bookstore.urls'</span><span class="p">)),</span>
)
and this is the urls.py file you need to create in the application directory that translate to django the mappings we have planned before:
PROJECT_ROOT/bookstore/urls.py
#from django from django.conf.urls.defaults import *# from bookstore from bookstore.models import Book
urlpatterns = patterns('', url(r'^$', 'bookstore.views.books', name="all_books"), url(r'^(d+)/book/$', 'bookstore.views.book', name="describe_book"), url(r'^your_books/$', 'bookstore.views.your_books', name="your_books"), url(r'^user_books/(?P<username>w+)/$', 'bookstore.views.user_books', name="user_books"), # CRUD urls url(r'^add/$', 'bookstore.views.add_book', name="add_book"), url(r'^(d+)/update/$', 'bookstore.views.update_book', name="update_book"), url(r'^(d+)/delete/$', 'bookstore.views.delete_book', name="delete_book"), )
A few notes about the urls:
- each url maps a uri (in a regular expression form) to a Django view. For example in the case of the url named your_books the request made to Django will be managed by a view named your_books
- note that in some case there is a parameter (an integer or a string) in the regular expression: that is for the identifier of a book or for the name of the user. These parameters are passed to the view
Writing the views
Now that you have planned all the views composing the application, it is time to actually write them!
PROJECT_ROOT/bookstore/views.py
#from django from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib.auth.decorators import login_required from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User#from bookstore from bookstore.models import Book from bookstore.forms import BookForm
def books(request): """ Return the all books list, ordered by added date. """ books = Book.objects.all().order_by("-added") return render_to_response("bookstore/books.html", { "books": books, "list": 'all', }, context_instance=RequestContext(request))
def user_books(request, username): """ Return an user books list. """ user = get_object_or_404(User, username=username) userbooks = Book.objects.filter(adder=user).order_by("-added") return render_to_response("bookstore/books.html", { "books": userbooks, "list": 'user', "username": username, }, context_instance=RequestContext(request))
def book(request, book_id): """ Return a book given its id. """ isyours = False book = Book.objects.get(id=book_id) if request.user == book.adder: isyours = True return render_to_response("bookstore/book.html", { "book": book, "isyours": isyours, }, context_instance=RequestContext(request))
@login_required def your_books(request): """ Return the logged user books list. """ yourbooks = Book.objects.filter(adder=request.user).order_by("-added") return render_to_response("bookstore/books.html", { "books": yourbooks, "list": 'yours', }, context_instance=RequestContext(request))
@login_required def add_book(request): """ Add a book to the bookstore. """ # POST request if request.method == "POST": book_form = BookForm(request.POST, request.FILES) if book_form.is_valid(): # from ipdb import set_trace; set_trace() new_book = book_form.save(commit=False) new_book.adder = request.user new_book.save() request.user.message_set.create(message=_("You have saved book '%(title)s'") % {'title': new_book.title}) return HttpResponseRedirect(reverse("bookstore.views.books")) # GET request else: book_form = BookForm() return render_to_response("bookstore/add.html", { "book_form": book_form, }, context_instance=RequestContext(request)) # generic case return render_to_response("bookstore/add.html", { "book_form": book_form, }, context_instance=RequestContext(request))
@login_required def update_book(request, book_id): """ Update a book given its id. """ book = Book.objects.get(id=book_id) if request.method == "POST": book_form = BookForm(request.POST, request.FILES, instance=book) book_form.is_update = True if request.user == book.adder: #from ipdb import set_trace; set_trace() if book_form.is_valid(): book_form.save() request.user.message_set.create(message=_("You have updated book '%(title)s'") % {'title': book.title}) return HttpResponseRedirect(reverse("bookstore.views.books")) else: book_form = BookForm(instance=book) return render_to_response("bookstore/update.html", { "book_form": book_form, "book": book, }, context_instance=RequestContext(request))
@login_required def delete_book(request, book_id): """ Delete a book given its id. """ book = get_object_or_404(Book, id=book_id) if request.user == book.adder: book.delete() request.user.message_set.create(message="Book Deleted")
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s">"bookstore.views.books"</span><span class="p">))</span>
Some notes on the views.py file you just created:
- some of the views can be accessed without authenticating to the system: books, user_books, book
- some of the views need authentication to the system: your_books, add_book, update_book, delete_book. In this case you need to use the @login_required decorator
- some of the views have additional input parameter to the request. For example add_book, update_book, delete_book need a book_id parameter to know which book to process
- all of the views define a response defined by a template and some variable that the template must process. For example the user_books renders the response with the books.html template and these variables processed by the response: books, list, username
Note that you also need to create a forms.py file and to add the BookForm that is used from the add_book and update_book views:
PROJECT_ROOT/bookstore/forms.py
#from django from django import forms from django.utils.translation import ugettext_lazy as _#from bookstore from bookstore.models import Book
class BookForm(forms.ModelForm): """ Book Form: form associated to the Book model """
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="nb">super</span><span class="p">(</span><span class="n">BookForm</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">is_update</span> <span class="o">=</span> <span class="bp">False</span>
<span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">""" Do validation stuff. """</span>
<span class="c"># title is mandatory</span>
<span class="k">if</span> <span class="s">'title'</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">:</span>
<span class="k">return</span>
<span class="c"># if a book with that title already exists...</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">is_update</span><span class="p">:</span>
<span class="k">if</span> <span class="n">Book</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s">'title'</span><span class="p">])</span><span class="o">.</span><span class="n">count</span><span class="p">()</span> <span class="o">></span> <span class="mi">0</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">forms</span><span class="o">.</span><span class="n">ValidationError</span><span class="p">(</span><span class="n">_</span><span class="p">(</span><span class="s">"There is already this book in the library."</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">cleaned_data</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">'coverart'</span><span class="p">,</span> <span class="s">'publisher'</span><span class="p">,</span> <span class="s">'author'</span><span class="p">,</span> <span class="s">'description'</span><span class="p">,</span> <span class="s">'title'</span><span class="p">,</span> <span class="s">'tags'</span><span class="p">)</span>
Here the form is generated by the model (DRY, Don't Repeat Yourself!). Note that the clean method implements some validation stuff. At this time we do not want a book to be created (or updated) if:
- title is empty
- there is already a book with that title
It is now time to write the application templates, but before doing so let's fix the way the Bookstore application css and images are managed. Also, before writing the templates, it is finally time to plugin the Bookstore application into Pinax!
Bookstore css and images
Create a bookstore directory in the PROJECT_ROOT/media directory. Inside the bookstore directory create a css directory and an img directory. Create a bookstore.css file in the css directory like this one:
PROJECT_ROOT/media/bookstore/css/bookstore.css
/* BOOKSTORE */
table.narrow {
width: 500px;
}
table.bookstore td {
vertical-align: top;
padding: 5px;
}
table.bookstore td h2 {
margin: 0;
padding: 0;
}
table.bookstore td.vote {
width: 80px;
text-align: center;
vertical-align: middle;
}
.bookstore .even {
background-color: #FAFAFA;
}
.bookstore .odd {
background-color: #F3F3F3;
}
div.url {
color: #666;
font-size: 90%;
font-style: italic;
}
Plugging the bookstore application into Pinax
To to so, you just need to modify site_base.html to include the the bookstore.css and the bookstore application (you just need to add two lines):
PROJECT_ROOT/templates/site_base.html:
{% block extra_head_base %}
<link rel="stylesheet" href="{{ STATIC_URL }}css/site_tabs.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/avatar.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/blogs.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/comments.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/friends.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/groups.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/locations.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/messages.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/microblogging.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/pagination.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/photos.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/tabs.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/topics.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/wiki.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}pinax/css/jquery.autocomplete.css" />
<link rel="stylesheet" href="{{ STATIC_URL }}bookmarks/css/bookmarks.css" />
<!-- here you insert the boookstore css-->
<link rel="stylesheet" href="{{ STATIC_URL }}bookstore/css/bookstore.css" />
<!-- end -->
{% block extra_head %}{% endblock %}
{% endblock %}
....
{% block right_tabs %}
{% if user.is_authenticated %}
<ul class="tabs">{% spaceless %}
<li id="tab_profile"><a href="{% url profile_detail user %}">{% trans "Profile" %}</a></li>
<li id="tab_photos"><a href="{% url photos %}">{% trans "Photos" %}</a></li>
<li id="tab_blogs"><a href="{% url blog_list_all %}">{% trans "Blogs" %}</a></li>
<li id="tab_tribes"><a href="{% url tribe_list %}">{% trans "Tribes" %}</a></li>
<li id="tab_tweets"><a href="{% url tweets_you_follow %}">{% trans "Tweets" %}</a></li>
<li id="tab_bookmarks"><a href="{% url all_bookmarks %}">{% trans "Bookmarks" %}</a></li>
<!-- here you insert the boookstore tab-->
<li id="tab_bookstore"><a href="{% url all_books %}">{% trans "Bookstore" %}</a></li>
<!-- end -->
<li id="tab_swaps"><a href="{% url offer_list_all %}">{% trans "Swaps" %}</a></li>
<li id="tab_locations"><a href="{% url loc_yours %}">{% trans "Locations" %}</a></li>
<li id="tab_inbox"><a href="{% url messages_inbox %}">{% trans "Inbox" %} ({{ combined_inbox_count }})</a></li>
{% endspaceless %}</ul>
{% endif %}
{% endblock %}
Writing the templates
Writing the base template
First you create a base template for the Bookstore application. This base template will be extend by all the Bookstore application templates.
PROJECT_ROOT/templates/bookstore/base.html:
{% extends "site_base.html" %}
{% load i18n %}
{% block rtab_id %}id="bookstore_tab"{% endblock %}
{% block subnav %}
{% endblock %}
$$/code
Note that this base template extends the site_base.html template you have edited a while ago. As you can easily deduct, the Bookstore application will have 3 sub menus: "All books", "Your books" and "Add a book".
Creating a template for listing the books
This is the template that the application will use to render the books, your_books and user_books views. Before writing any template, you need to create the directory that will actually contain them: create a bookstore directory in the templates directory of the project. Also you need to create a PROJECT_ROOT/site_media/media/bookstore/thumb_100_100 directory to manage thumbnails, according to the way the get_thumb_url method is written (you could make things easily customisable with a variable in settings.py).
Now it is time to create the books.html, the template for listing the books:
PROJECT_ROOT/templates/bookstore/books.html template
{% extends "bookstore/base.html" %}
{% load i18n %}
{% block head_title %}{% blocktrans %}Library{% endblocktrans %}{% endblock %}
{% block body %}
<span class="nt"><h1></span>
{% ifequal list 'all' %}
{% trans "All Books" %}
{% endifequal %}
{% ifequal list 'user' %}
{{ username }} {% trans "books" %}
{% endifequal %}
{% ifequal list 'yours' %}
{% trans "Your Books" %}
{% endifequal %}
<span class="nt"></h1></span>
<span class="c"><!-- alternate --></span>
{% if books %}
<span class="nt"><table</span> <span class="na">class=</span><span class="s">"bookstore"</span><span class="nt">></span>
{% for book in books %}
<span class="nt"><tr</span> <span class="na">class=</span><span class="s">"{% cycle odd,even %}"</span><span class="nt">></span>
<span class="c"><!-- meta --></span>
<span class="nt"><td</span> <span class="na">class=</span><span class="s">"meta"</span> <span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"details"</span><span class="nt">></span>{% blocktrans %}added by{% endblocktrans %} <span class="nt"><a</span> <span class="na">href=</span><span class="s">"{% url profile_detail book.adder.username %}"</span><span class="nt">></span>{{ book.adder }}<span class="nt"></a></div></span>
{% blocktrans %}on{% endblocktrans %} {{ book.added|date }}
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"/bookstore/user_books/{{ book.adder.username }}"</span><span class="nt">><i></span>{% blocktrans %}View all books added by {% endblocktrans %}{{ book.adder }}<span class="nt"></i></a></span>
<span class="nt"></td></span>
<span class="c"><!-- book info --></span>
<span class="nt"><td></span>
<span class="nt"><h3><a</span> <span class="na">href=</span><span class="s">"/bookstore/{{ book.id }}/book/"</span><span class="nt">></span>{{ book.title }}<span class="nt"></a></h2></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"body"</span><span class="nt">></span>
<span class="nt"><strong><i></span>{{ book.publisher }}<span class="nt"><br</span> <span class="nt">/></span>{{ book.author }}<span class="nt"><br</span> <span class="nt">/></strong></i></span>
{{ book.description|linebreaks|truncatewords:50 }}
<span class="nt"></div></span>
{% ifequal list 'yours' %}
<span class="nt"><table></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>
<span class="c"><!-- udpate book --></span>
<span class="nt"><form</span> <span class="na">method=</span><span class="s">"GET"</span> <span class="na">action=</span><span class="s">"{% url update_book book.id %}"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">value=</span><span class="s">"{% trans "</span><span class="na">Update</span> <span class="na">Book</span><span class="err">"</span> <span class="err">%}"</span> <span class="nt">/></span>
<span class="nt"></form></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="c"><!-- delete book --></span>
<span class="nt"><form</span> <span class="na">method=</span><span class="s">"POST"</span> <span class="na">action=</span><span class="s">"{% url delete_book book.id %}"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">value=</span><span class="s">"{% trans "</span><span class="na">Delete</span> <span class="na">Book</span><span class="err">"</span> <span class="err">%}"</span> <span class="nt">/></span>
<span class="nt"></form></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
{% endifequal %}
<span class="nt"></td></span>
<span class="c"><!-- cover art --></span>
<span class="nt"><td></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"coverart"</span> <span class="nt">></span>{{ book.thumb|safe }}<span class="nt"></div></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
{% endfor %}
<span class="nt"></table></span>
{% else %}
<span class="nt"><p></span>{% trans "No books yet." %}<span class="nt"></p></span>
{% endif %}
{% endblock %}
Notes about this template:
- the 'list' variable is used to define the title of the page ("All books", "Your books", "An user books")
- the 'books' collection is iterated to create a table with a row for each book (the collection may be related to the whole book set, to your book set or to a user book set
- each row contains the following sections (one for each column): meta, book info and coverart
- the meta section displays the user that added the book (with a link to her/his profile), the date when the book has been added and a link to the full book list added by that user
- the book info section displays the book's title with a link to its page, the publisher, the author and the description (truncated after 50 characters). If the template is rendering the your_books view (thus if 'list' == 'Your books'), a link for updating/deleting that book is displayed
- if the books collection is empty the table is not created an the user is warned with a "No books yet." message
Creating a template for viewing a book
This is maybe the easiest of the templates you need to create. Create the book.html template using the following code:
PROJECT_ROOT/templates/bookstore/book.html template
{% extends "bookstore/base.html" %}
{% load i18n %}
{% load uni_form %}
{% block head_title %}{% blocktrans %}Book Description{% endblocktrans %}{% endblock %}
{% block body %}
<h1>{{ book.title }}</h1>
<p>
<div class="coverart" >{{ book.fullpicture|safe }}</div>
</p>
<p>
<!-- book info -->
<h3>{{ book.title }}</h2>
<div class="body">
<strong><i>{{ book.publisher }}<br />{{ book.author }}<br /></strong></i>
{{ book.description|linebreaks }}
</div>
</p>
<p>
<!-- book action -->
{% if isyours %}
<table>
<tr>
<td>
<!-- udpate book -->
<form method="GET" action="{% url update_book book.id %}">
<input type="submit" value="{% trans "Update Book" %}" />
</form>
</td>
<td>
<!-- delete book -->
<form method="POST" action="{% url delete_book book.id %}">
<input type="submit" value="{% trans "Delete Book" %}" />
</form>
</td>
</tr>
</table>
{% endif %}
</p>
<p class="meta">
<!-- meta -->
<div class="details">{% blocktrans %}added by{% endblocktrans %} <a href="{% url profile_detail book.adder.username %}">{{ book.adder }}</a></div>{% blocktrans %}on{% endblocktrans %} {{ book.added|date }}
</p>
{% endblock %}
It is a bit like the books.html template, but here Django is rendering only one book. You may note that:
- the variable book is used to gain information about the book to render
- the variable isyours is needed to let the user to update or delete the book if she/he is the user who has added it
- there are 3 sections in the book representation: book info, book action, meta
- book info displays the book's cover art, title, publisher, author and description
- book action displays the buttons to update or delete the book if the user is the one who has added it in the system
- meta displays the user that added the book (with a link to his/her profile) and the date when the book has been added
Creating a template for adding a book
Now create the add.html template, for adding new books to the library:
PROJECT_ROOT/templates/bookstore/add.html template
{% extends "bookstore/base.html" %}
{% load i18n %}
{% load uni_form %}
{% block head_title %}{% blocktrans %}Add Book{% endblocktrans %}{% endblock %}
{% block body %}
<h1>{% trans "Add Book" %}</h1>
<span class="nt"><form</span> <span class="na">enctype=</span><span class="s">"multipart/form-data"</span> <span class="na">method=</span><span class="s">"POST"</span> <span class="na">action=</span><span class="s">""</span> <span class="na">class=</span><span class="s">"uniForm"</span><span class="nt">></span>
<span class="nt"><fieldset</span> <span class="na">class=</span><span class="s">"inlineLabels"</span><span class="nt">></span>
{{ book_form|as_uni_form }}
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form_block"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">value=</span><span class="s">"{% trans 'add book' %}"</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"></fieldset></span>
<span class="nt"></form></span>
{% endblock %}
Here there is nothing much to explain, you are rendering the template with a BookForm variable.
Creating a template for updating an existing book
Time to create the update template, for updating a book in the library:
PROJECT_ROOT/templates/bookstore/update.html template
{% extends "bookstore/base.html" %}
{% load i18n %}
{% load uni_form %}
{% block head_title %}{% blocktrans %}Add Book{% endblocktrans %}{% endblock %}
{% block body %}
<h1>{% trans "Update Book: " %}{{ book.title }}</h1>
<form enctype="multipart/form-data" method="POST" action="" class="uniForm">
<p align='center'>{{ book.fullpicture|safe }}</p>
<fieldset class="inlineLabels">
{{ book_form|as_uni_form }}
<div class="form_block">
<input type="submit" value="{% trans 'update book' %}">
</div>
</fieldset>
</form>
{% endblock %}
Basically this template is like the add.html ones, with another variable - the book - coming from the view.
A quick tour of the basic bookstore application
Now that you are over writing the models, the view and the templates of the application, you may finally test if everything is working properly.
If you still didn't do so, start the development server and try visiting the pages and access the features you have implemented.
What's next
Now that you are ready with the basic bookstore application, you may read the next tutorial parts (as soon as they will appear at this blog) in order to implement other features like localization, pagination, avatars, profiles, voting, tagging, feeds, comments, notifications, and flags for contents.
Where can I get the code?
The whole tutorial is at gitHub: http://github.com/capooti/pinaxtutorial There you can download tutorial's code, and the tutorial text in reStructured Text format (Sphinx ready).