Saturday, January 7, 2017

Django - 014 - Our third view - dynamic URLs

In our current_datetime view, the contents of the page - the current date/time - were dynamic, but the URL (/time/) was static. In most dynamic web applications though, a URL contains parameters that influence the output of the page. For example, an online bookstore might give each book its own URL, like /books/243/ and /books/81196/. Let us create a third view that displays the current date and time one hour into the future, the page /time/plus/2/ displays the date/time two hours into the future, the page time/plus/3/ displays the date/time three hours into the future, and so on. A novice might think to code a separate view function for each hour offset, which might result in a URLconf like this:

urlpatterns = [
                 url(r'^time/$', current_datetime),
                 url(r'^time/plus/1/$', one_hour_ahead),
                 url(r'^time/plus/2/$', two_hours_ahead),
                 url(r'^time/plus/3/$', three_hours_ahead),

              ]

Clearly, this line of thought is flawed. Not only would this result in redundant view functions, but also the application is fundamentally limited to supporting only the predefined hour ranges - one, two or three hours. 

If we decided to create a page that displayed the time four hours into the future, we'd have to create a separate view and URLconf line for that, furthering the duplication. 

How, then do we design our application to handle arbitrary hour offsets? The key is to use wildcard URL patterns. As I mentioned previously, a URL pattern is a regular expression; hence, we can use the regular expression pattern \d+ to match one or more digits:

urlpatterns = [
                 #...
                 url(r'^time/plus/\d+/$', one_hour_ahead),
                 #...

              ]

This new URL pattern will match any URL such as /time/plus/4/ , /time/plus/43/ or even /time/plus/800000000/ . Come to think of it, let us limit it so that the maximum allowed offset is something reasonable.

In this example, we will set a maximum 99 hours by only allowing either one or two digit numbers - and in regular expression syntax, that translates into \d{1,2}:

url(r'^time/plus/\d{1,2}/$', hours_ahead),

Now that we've designated a wildcard for the URL, we need a way of passing that wildcard data to the view function, so that we can use a single view function for any arbitrary hour offset. We do this by placing parentheses around the data in the URLpattern that we want to save. In the case of our example, we want to save whatever number was entered in the URL, so let us put parenthesis around the \d{1,2} , like this:

url(r'^time/plus/(\d{1,2})/$', hours_ahead),

If you are familiar with regular expressions, you will be right at home here; we are using parentheses to capture data from the matched text. The final URLconf, including our previous two views, looks like this:

from django.conf.urls import include, url
from django.contrib import admin
from myD3site.views import hello
from myD3site.views import my_homepage_view , current_datetime, read_file_info, hours_ahead

urlpatterns = [

                        url(r'^admin/', include(admin.site.urls)), 
                        url(r'^hello/$', hello),
                        url(r'^$',my_homepage_view),
                        url(r'^time/$',current_datetime),
                        url(r'^fileInfo/$',read_file_info),
                        url(r'^another-time-page/$',current_datetime),
                        url(r'^time/plus/(\d{1,2})/$', hours_ahead),

]

If you are experienced in another web development platform, you may be thinking, "Hey, let us use a query string parameter!" - something like /time/plus?hours=3, in which the hours would be designated by the hours parameter in the URL's query string (the part of the '?') . You can do that with Django, but one of Django's core philosophies is that URLs should be beautiful. The URL /time/plus/3 is far cleaner, simpler, more readable, easier to recite to somebody aloud and just plain prettier than its query string counterpart. Pretty URLs are a characteristic of a quality web application. 

Django's URLconf system encourages pretty URLs by making it easier to use pretty URLs than not to.

With that taken care of, let us write the hours_ahead view. hours_ahead is very similar to the current_datetime view we wrote earlier, with a key difference: it takes an extra argument, the number of hours of offset. Here is the view code:

from django.http import HttpResponse
import datetime
import os
from django.http import Http404

def hours_ahead(request, offset):
  try:
     offset = int(offset)
  except ValueError:
     raise Http404()
     dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
     html = "<html><body> In %s hour(s), it will be %s. </body></html>" % (offset,dt)

     return HttpResponse(html)


Let us take a closer look at this code.

The view function, hours_ahead, takes two parameters: request and offset:
  • request is an HttpRequest object, just as in hello and current_datetime. I will say it again: each view always takes an HttpRequest object as its first parameter. 
  • offset is the string captured by the parenthesis in the URLpattern. For example, if the requested URL were /time/plus/3/, then offset would be the string '3'. If the requested URL were /time/plus/21/, then offset would be the string '21'. Note that captured values will always be Unicode objects, not integers, even if the string is composed of only digits, such as '21'.

I decided to call the variable offset, but you can call it whatever you like, as long as it is a valid Python identifier. The variable name does not matter, all that matters is that it is the second argument to the function, after request. (It is also possible to use keyword, rather than positional, arguments in a URLconf.) 

The first thing we do within the function is call int() on offset. This converts the Unicode string value to an integer. 

Note that Python will raise a ValueError exception if you call int() on a value that cannot be converted to an integer, such as the string foo. In this example, if we encounter the ValueError, we raise the exception django.Http404, which, as you can imagine, results in a 404 Page not found error.

Astute readers will wonder: how could we ever reach the ValueError case, anyway, given that the regular expression in our URLpattern - (\d{1,2}) - captures only digits, and therefore offset will only ever be a string composed of digits?  The answer is, we won't, because the URLpattern provides a modest but useful level of input validation, but we still check for the ValueError in case this view function ever gets called in some other way.

It is good practice to implement view functions such that they do not make any assumptions about their parameters. Loose coupling, remember?

In the next line of the function, we calculate the current date/time and add the appropriate number of hours. We have already seen datetime.datetime.now() from the current_datetime view; the new concept here is that you can perform date/time arithmetic by creating a datetime.timedelta object and adding to a datetime.datetime object. Our result is stored in the variable dt.

This line also shows why we called int() on offset- the datetime.timedelta function requires the hours parameter to be an integer. 

Next, we construct the HTML output of this view function, just as we did in current_datetime. A small difference in this line from the previous line is that it uses Python's format-string capability with two values, not just one. Hence, there are two %s symbols in the string and a tuple of values to insert: (offset,dt)

Finally, we return an HttpResponse of the HTML.

With that view function and URLconf written, start the Django development server (if it is not already running), and visit http://127.0.0.1:8000/time/plus/3/ to verify it works.



Finally, visit http://127.0.01:8000/time/plus/100/ to verify that the pattern in your URLconf only accepts one or two digit numbers; Django should display a Page not Found error in this case, just as we saw in the section A quick note about 404 errors earlier.



The URL http://127.0.0.1/time/plus/(with no hour designation)  should also throw a 404.











No comments:

Post a Comment