Step 9: Write the View
I don't want to get to much into Django background and philosophy (there is plenty of info on the web on what Django is about already). But, just a little idea would be in order here.
Django is more than a CMS, it is a web framework and if you are familiar with MVC, Django follows a similar idea. Django has come up with it's own way of expressing it - MTV (Model, Template, View). If you aren't familiar with CMS and MVC, look them up, there is plenty of info on the web. I'm only going to talk about Django's way here.
The idea behind this MTV approach is that we work with three aspects to create our websites. The Model expresses the database side (as you saw when we created the models.py file). The Template is the HTML, CSS and everything needed to serve that up. The plug between the two is the view, which, in Django's way of doing things, is the logic.
To put it in really simple terms, we have stuff in our database (models), we have ways to show the stuff (templates) and we have the logic of what stuff to show where (views).
Good, that's all I'll say about that. You can read more, just Google it, or follow the link I gave above. But for now, forget about it, and lets concentrate on creating the views for our blog.
Our views.py file is in the blog app, remember. Open it up in a text editor. I am sure you know how to do that by now. You can open up a second Terminal at this point, to keep one running our development server.
Bear with me on the views, because I think it is the part of the application where you need to really understand what you are doing. Django comes with generic view, where common logic is bundled together and you don't need to program your own view at all, and this stuff is actually great for blog, but for now, lets get down and dirty and write the logic out ourselves. There is a lot of advantage to this, of course… you can twist your site your way and do really fun things. You'll want to really get into Python properly in order to take full advantage of the capability provided by the views.
Django gives you lots of ways of doing things, but in the views we have a special class called render_to_response. We can sort our logic out in the view, and pass it on using the render_to_response method. It is a part of the django.shortcuts package, which provides, you guessed, shortcuts. So our first step is to import that into our view.
from django.shortcuts import render_to_response
Next, we need the stuff from our database. No nasty database code needed. We'll just import the models classes and get on with it. We can do a wildcard (with '*') import, but remember they were Post, Tags and Category.
from django.shortcuts import render_to_response
from zing.blog.models import *
We are going to import two more thing that we'll get back to later, the HttpResponseRedirect and datetime. The HttpResponseRedirect allows us to redirect pages, and you'll see why later on. The datetime is useful when we are working with, you guessed, dates.
from django.shortcuts import render_to_response
from zing.blog.models import *
from django.http import HttpResponseRedirect
Okay, if you were building your own blog at this point you should pause, perhaps get away from a computer and think really hard about what you want to serve up, and how. Dream up the url patterns, and all the bits of contents, and then work on making python serve that up by passing in chunks of code that your template can read. In this case, of course, you don't know how to do that, so lets make a list of wants, and then we'll go about crafting them.
Okay, for this blog application, here is our list:
-
- What: A home page with the latest 7 posts. Why 7? It really doesn't matter. For this blog we are going with 7.
- How: http://site_name/ This is our homepage.
-
- What: A detail page, where we can view the whole post and add comments.
- How: http://site_name/year/month/slug/ So we'll have the year (four numbers, like 2010), the month (two numbers, like 07) and the post slug (remember that from our models.py file? This is where we are going to use it.
-
- What: A page listing all months of a given year.
- How: http://site_name/year/ This is going to list all the months of that year, which can be clicked to see all the posts on that month
-
- What: A page listing all posts by month.
- How: http://site_name/year/month/ This is going to list all the months of that year, which can be clicked to see all the posts on that month
-
- What: Archive pages, going back after the first page in chunks of 10 (just for a rounder number).
- How: http://site_name/archive/number/ The number refers to numbered archive pages. Our home page shows seven, so archive page 1 (http://site_name/archive/1/) is going to show us the posts 8 to 17. We could duplicate (or put a permanent redirect.)
-
- What: A page listing all the categories.
- How: http://site_name/category/
-
- What: A page listing all the posts per category.
- How: http://site_name/category/category_name/ Category_name refers, of course, to the name of the category.
-
- What: A page listing all the tags.
- How: http://site_name/tags/ A page listing all the tags
-
- What: A page listing all the posts tagged with a specific tag.
- How: http://site_name/tags/tag_name/ The tag_name is the name of the tag. A page that lists all the posts for that tag.
We also want a sidebar, which we'll 'include' which will have navigation, RSS feed, a list of categories and a list of tags. This doesn't affect what we put in our views, but it will be the main way that some of those pages are linked to.
Note that the month and year patterns mean that if someone chops off the url at the end, they get another useful page. We don't actually want to create a link to the year page (It's not much use), but we want a site that has intelligent urls in place, and hense any URL that is cut short should be worth something and contribute to our website.
Okay, let's build the core functionality. We don't build the urls in the views, but I find it is a really good way to think about it. So lets just put them in as comments. We'll put the archives and the home page in one function, because they are doing the same thing. You'll soon see what I mean. We are not really following the DRY principal (don't repeat yourself), but most of our repeating is really simple stuff, and it helps us understand exactly what we are building. You are more than welcome to combine the functions afterwords, as you see fit.
from django.shortcuts import render_to_response
from zing.blog.models import *
#http://site_name/ and http://site_name/archives/number/
#http://site_name/year/month/slug/
#http://site_name/year/month
#http://site_name/year/
#http://site_name/category
#http://site_name/category/category_name
#http://site_name/tag/tag_name
#http://site_name/tag/
No we need to build a function around each of those, that gets the request (which comes from the user's browser - saying give me the page. So our first line is a function call, with one arguement, the request object. Don't worry if you don't understand, just follow along. It should all become more clear as we move along.
We need to serve a page and have a way to stick stuff in it, and our render_to_response gives us one quick, easy way to do just that.
So, lets fill out our views.py much more. This is starting to look like a lot, but trust me, it isn't hard to understand so far. We are just doing a lot of the same thing. When you become more proficient at it, you'll combine a lot of these, but I want to spell it all out.
from django.shortcuts import render_to_response
from zing.blog.models import *
def list(request):
#http://site_name/ and http://site_name/archives/number/
return render_to_response('base.html', {},)
def detail(request):
#http://site_name/year/month/slug/
return render_to_response('base.html', {},)
def month(request):
#http://site_name/year/month
return render_to_response('base.html', {},)
def year(request):
#http://site_name/year/
return render_to_response('base.html', {},)
def category(request):
#http://site_name/category
return render_to_response('base.html', {},)
def one_category(request):
#http://site_name/category/category_name/
return render_to_response('base.html', {},)
def one_tag(request):
#http://site_name/tag/tag_name/
return render_to_response('base.html', {},)
def tag(request):
#http://site_name/tag/
return render_to_response('base.html', {},)
Okay, to make it more readable, we are going to break it up a bit. I'll build each function by itself first.
Note that we have an html file in the return value of the function. I have just put in "base.html" but actually we are going to change that - you'll see.
Lets first do the list page. So we want 7 items on this page and a link to the next lot in the archives, and a previous link when we go further down the archive. Does that make sense? If you don't understand what I am doing, I think just following along at this point will soon clear it all up.
We are going to need to import one other thing on this page, the archive page number. Because our first page doesn't have a number we can use one of the wonderful little tricks of Python, give a default value. I'll build this first function and then explain.
def list(request, archive=1):
#http://site_name/ and http://site_name/archives/number/
page = (int(archive)*7)
if Post.objects.all().count() > page + 7:
posts = Post.objects.order_by('-published')[page-7:page]
else:
posts = Post.objects.order_by('-published')[page-7:]
# next and previous archive pages
if Post.objects.all().count() > page
next = (int(archive)) + 1
else:
next = 0
previous = (int(archive)) - 1
return render_to_response('base.html', {'posts':posts,
'next':next,
'previous':previous,
},)
In the function declaration at the top, we assign a value to archive as a default value. If the url contains /archive/some_number/ then the number will be passed to archive (we'll get to how exactly that happens when we talk about the urlconfig later on).
The next thing is the page variable, where we are creating a variable that is going to give us chunks of seven. So, if we go to archive page 3, we are going to be looking at posts 22 onwards, and so on.
If you have any familiarity with programming at all, you'll know about types (ints, strings, floats and so on). We have to change the value of the archive variable to an int (type coercion). This is because what we get from the url is a string.
Then we have an if statement. This is just in-case we have to few posts in our database to show seven posts of the first page. So, if we have enough we are going to show seven posts. Remember the Models.py file. We imported everything from zing.blog.models, and this included Post. Now we are actually calling our database and getting a set of objects that match our query. Easy, ha?
Post.objects.order_by('-published')[page + 7:page]
It is rather simple to understand what we did here.
the 'order_by' does exactly what it says, and can order based on numerals or alphabetical.
In our case, we have a value 'published' for each object, and by adding a minus in front we get the reverse order.
'-published'.
If you know a little Python, you'll know about list slicing. If you don't, it is a useful thing to know about, try reading up on it a bit.
I did some funny logic here, with one multiplying out to be seven, and then I delete 7 again to get the first post. Just trust me that it works better this way.
If we don't have seven items on that page yet, we can just chop it off at whatever the end value is (hence the list slicing with a missing end number - in Python this just means "give me everything from my starting value until the end").
There is one small problem that we can take care of from here as well, and that is how to know what the next page and previous pages are. We'll put a comment in, just so we know we are now dealing with the next bit of logic.
All we need to do is pass the value more than and less than the 'archive' variable. Simple to do, but there is a catch, we also want to know when we don't have any more archive pages. We solve this for next with a simple if statement and make sure that there are still values left. For 'previous' we don't actually have to worry, and we'll get to how we work with these guys later.
It should become clear that there is one last annoyance. If we are back at archive page 1, we don't want the url to be site_name/archive/1/, but rather we would like to redirect that page to the homepage.
We'll fix that with a simple redirect. Have a look back at the last thing we imported when we first started working on the views.py file.
def list(request, archive=1):
#http://site_name/ and http://site_name/archives/number/
if archive == "1":
return HttpResponseRedirect("/")
page = (int(archive)*7)
if Post.objects.all().count() > page + 7:
posts = Post.objects.order_by('-published')[page-7:page]
else:
posts = Post.objects.order_by('-published')[page-7:]
# next and previous archive pages
if Post.objects.all().count() > page
next = (int(archive)) + 1
else:
next = 0
previous = (int(archive)) - 1
return render_to_response('base.html', {'posts':posts,
'next':next,
'previous':previous,
},)
As simple as that. We tested for a string (that is why the one was in quotes) to see if it came like that from the url.
Alright, the next stop is our detail page. This is the one where we actually show the single blog post.
def detail(request, sl):
#http://site_name/year/month/slug/
try:
post = Post.objects.filter(slug=sl)[0]
try:
previous_post = post.get_previous_by_published()
except:
previous_post = ""
try:
next_post = post.get_next_by_published()
except:
next_post = ""
except:
next_post = ""
previous_post = ""
post = ""
return render_to_response('base.html', {'post':post,
'next_post':next_post,
'previous_post':previous_post,
},)
As you can see, getting the post value is so simple. You'll see we put a '[]' at the end. This allows us to return one value, rather than a whole queryset, so we can call it directly from the variable, not having to iterate over it.
The more difficult stuff (well, it isn't difficult) is getting the previous and next posts. Django makes this super easy for us by providing as 'get_next_by' function that we can use to determine the next and previous posts.
Again, when we run out, we assign Null values to avoid errors. You can see that I build the whole thing with error handling (the try/except). As you know, we don't want to break our website because of stupid coding issues.
The way that we derived the post for this url just ignores the year and the date. That means that if someone enters the wrong year, but gets the slug right, they will still see the right blog post. To change that you would need to pass in more parameters from the url, and filter the results further. I'm not going to worry about that here. You should be able to do that yourself without any issue by the time you have finished this tutorial.
The next one is a simple one, getting the posts for a given month.
def month(request, year, month):
#http://site_name/year/month/
date = datetime.datetime(int(year), int(month), 1)
try:
posts = Post.objects.order_by('-published').filter(published__year=year).filter(published__month=month)
except:
posts = ""
return render_to_response('base.html', {'posts':posts,
'date':date,
},)
Of course you need the year in the equation as well for finding the month - the given month is repeated each year, so we filter both for year and for month.
You can keep working that section a lot. You can test if there are months that are written wrong, if the dates given are in the future and so on. And then return different error messages for them (you'll see when we get to templates). For now, let's just keep it simple.
Just to help us make nice title tags (wait till you are in the templates), we'll also pass in a date value. We need to give datetime.datetime a year, a month and a day. We don't have, or care, about the day (we don't need to derive it in our templates), so we'll just make it the first of whatever month we have, hence the 1.
Now, chop off the month and let's work with the year.
def year(request, year):
#http://site_name/year/
post_error = ""
year = int(year)
yr = datetime.datetime(year, 1, 1) months = 12
by_month = []
if Post.objects.filter(published__year=year).count():
if year == datetime.datetime.now().year:
months = datetime.datetime.now().month
for month in range(1, months+1):
by_month.append({datetime.datetime(year, month, 1):
Post.objects.filter(published__month=month).filter(published__year=year)})
elif year > datetime.datetime.now().year:
post_error = "It is not yet %d, try an earlier year." % year
else:
post_error = "There are no posts for %d." % year
return render_to_response('base.html', {'by_month':by_month,
'yr':yr,
'post_error':post_error,
},)
Under the year we want to show how many posts there are in each month of that year. This is easy for most years. The challange is the current year, because we don't just want to show results for months that are in the future.
So, if the year is the current year, we find out the month, then use that as the value to iterate through the rest of the months. Otherwise we just do 12 months - nice and easy.
The 'yr' value is just thrown in there so that we have it for the templates as well. That way we have a way to know which year we are talking about. You'll understand it better when we get there.
Next up is the category page. As we did above, we have two separate views to write. We could easily combine them, but I prefer to keep them separate for now.
So the first one is just the list of categories. Things are getting easy now.
def category(request):
#http://site_name/category/
return render_to_response('base.html', { 'categories':Category.objects.all(),},)
The only new thing there was that we called a different class in our model, Category. Otherwise, it is just to easy.
So, the next thing is to give posts by individual categories.
def one_category(request, category):
#http://site_name/category/category_name/
posts = Post.objects.order_by('-published').filter(categories__name=category.lower())
return render_to_response('base.html', {'posts':posts,
'category':category,
},)
This one is again really simple, but introduces something new. Do you notice the 'categories__name'. This is because the categories value in our models.py file is in the Category class. The double underscore says "Get me the name of the category". In this case there is only one thing in the Category class - name, but if there were more, you would still work with it the same way.
We also pass a value straight from the function to the return - the name of the category. We do this so that in our template we can say something like "Under x category we got these posts".
Another thing I did here was to make sure that what I passed as a value was lower case. It is a good idea to enter all names and things like that as lower case, and then you can just make them capitalized or whatever you need in your site. But, just in case someone capitalized it in the url, we'll solve their problem and make it lower case.
The last two, the tags, we will do basically exactly the same thing over again, only this time digging in the Tags class.
I'm going to assume that you can manage that on your own. Doing it on own also means that you need to read what I wrote above and understand it.