Home » Flask 教程 » The Flask Mega-Tutorial, Part XIII: Dates and Times
  • 28
  • 08月

This is the thirteenth article in the series in which I document my experience writing web applications in Python using the Flask microframework.

The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to call microblog.

Here is an index of all the articles in the series that have been published to date:

Table of Contents:

[TOC]

A quick note about github

For those that did not notice, I recently moved the hosting of the microblog application to github. You can find the repository at this location:

https://github.com/miguelgrinberg/microblog

I have added tags that point to each tutorial step for your convenience.

The problem with timestamps

One of the aspects of our microblog application that we have left ignored for a long time is the display of dates and times.

Until now, we just trusted Python to render the datetime objects in our User and Post objects on its own, and that isn't really a good solution.

Consider the following example. I'm writing this at 3:54PM on December 31st, 2012. My timezone is PST (or UTC-8 if you prefer). Running in a Python interpreter I get the following:

>>> from datetime import datetime
>>> now = datetime.now()
>>> print now
2012-12-31 15:54:42.915204
>>> now = datetime.utcnow()
>>> print now
2012-12-31 23:55:13.635874

The now() call returns the correct time for my location, while the utcnow() call returns the time in UTC units.

So which one is better to use?

If we go with now() then all the timestamps that we store in the database will be local to where the application server is running, and this presents a few problems.

One day we may need to move the server to another location across time zones, and then all the times will have to be corrected to the new local time in the database before the server can be restarted.

But there is a more important problem with this approach. For users in different timezones it will be awfully difficult to figure out when a post was made if they see times in the PST timezone. They would need to know in advance that the times are in PST so that they can do the proper adjustments.

Clearly this is not a good option, and this is why back when we started our database we decided the we would always store timestamps in the UTC timezone.

While standarizing the timestamps to UTC solves the issue of moving the server across timezones, it does not address the second issue, dates and times are now presented to users from any part of the world in UTC.

This can still be pretty confusing to many. Imagine a user in the PST timezone that posts something at 3:00pm. The post immediately shows up in his or her index page with a 11:00pm time, or to be more exact 23:00.

The goal of today's article is to address the issue of date and time display so that our users do not get confused.

User specific timestamps

The obvious solution to the problem is to individually convert all timestamps from UTC to local time for each user. This allows us to continue using UTC in our database so that we have consistency, while an on-the-fly conversion for each user makes times consistent for everyone.

But how do we know the location of each of our users?

Many websites have a configuration page where users can specify their timezone. This would require us to add a new page with a form in which we present users with a dropdown with the list of timezones. Users should be asked to enter their timezone when they access the site for the first time, as part of their registration.

While this is a decent solution that solves our problem, it is a bit cumbersome to ask our users to enter a piece of information that they have already configured in their systems. It seems it would be more efficient if we could just go grab the timezone setting from their computers.

For security reasons web browsers will not allow us to get into our users systems to obtain this information. Even if this was possible we would need to know where to find the timezone on Windows, Linux, Mac, iOS, and Android, without counting other less common operating systems.

Turns out the web browser knows the user's timezone, and exposes it through the standard Javascript APIs. In this Web 2.0 world it is safe to assume that users will have Javascript enabled (no modern site will work without scripting), so this solution has potential.

We have two ways to take advantage of the timezone configuration available via Javascript:

  • The "old-school" approach would be to have the web browser somehow send the timezone information to us when the user first logs on to the server. This could be done with an Ajax call, or much more simply with a meta refresh tag. Once the server knows the timezone it can keep it in the user's session and adjust all timestamps with it when templates are rendered.
  • The "new-school" approach would be to not change a thing in the server, which will continue to send UTC timestamps to the client browser. The conversion from UTC to local then happens in the client, using Javascript.

Both options are valid, but the second one has an advantage. The browser is best able to render times according to the system locale configuration. Things like AM/PM vs. 24 hour clock, DD/MM/YY vs. MM/DD/YY and many other cultural styles are all accessible to the browser and unknown to the server.

And if that wasn't enough, there is yet one more advantage for the new-school approach. Turns out someone else has done all the work for us!

Introducing moment.js

Moment.js is a small free and open source Javascript library that takes date and time rendering to another level. It provides every imaginable formatting option, and then some.

To use moment.js in our application we need to write a little bit of Javascript into our templates. We start by constructing a moment object from an ISO 8601 time. For example, using the UTC time from the Python example above we create a moment object like this:

moment("2012-12-31T23:55:13 Z")

Once the object is constructed it can be rendered to a string in a large variety of formats. For example, a very verbose rendering according to the system locale would be done as follows:

moment("2012-12-31T23:55:13 Z").format('LLLL');

Below is how your system renders the above date:

Tuesday, January 1 2013 7:55 AM

Here are some more examples of this same timestamp rendered in different formats:

FormatResult
L01/01/2013
LLJanuary 1 2013
LLLJanuary 1 2013 7:55 AM
LLLLTuesday, January 1 2013 7:55 AM
ddddTuesday

The library support for rendering options does not end there. In addition to format() it offers fromNow() and calendar(), with much more friendly renderings of timestamps:

FormatResult
fromNow()a year ago
calendar()01/01/2013

Note that in all the examples above the server is rendering the same UTC time, the different renderings were executed by your own web browser.

The last bit of Javascript magic that we are missing is the code that actually makes the string returned by moment visible in the page. The simplest way to accomplish this is with Javascript's document.write function, as follows:

<script>
document.write(moment("2012-12-31T23:55:13 Z").format('LLLL'));
</script>

While using document.write is extremely simple and straightforward as a way to generate portions of an HTML document via javascript, it should be noted that this approach has some limitations. The most important one to note is that document.write can only be used while a document is loading, it cannot be used to modify the document once it has completed loading. As a result of this limitation this solution would not work when loading data via Ajax.

Integrating moment.js

There are a few things we need to do to be able to use moment.js in our microblog application.

First, we place the downloaded library moment.min.js in the /app/static/js folder, so that it can be served to clients as a static file.

Next we add the reference to this library in our base template (file app/templates/base.html):

<script src="/static/js/moment.min.js"></script>

We can now add <script> tags in the templates that show timestamps and we would be done. But instead of that it that way, we are going to create a wrapper for moment.js that we can invoke from the templates. This is going to save us time in the future if we need to change our timestamp rendering code, because we will have it just in one place.

Our wrapper will be a very simple Python class (file app/momentjs.py):

from jinja2 import Markup

class momentjs(object):
    def __init__(self, timestamp):
        self.timestamp = timestamp

    def render(self, format):
        return Markup("<script>\ndocument.write(moment(\"%s\").%s);\n</script>" % (self.timestamp.strftime("%Y-%m-%dT%H:%M:%S Z"), format))

    def format(self, fmt):
        return self.render("format(\"%s\")" % fmt)

    def calendar(self):
        return self.render("calendar()")

    def fromNow(self):
        return self.render("fromNow()")

Note that the render method does not directly return a string but instead wraps the string inside a Markup object provided by Jinja2, our template engine. The reason is that Jinja2 escapes all strings by default, so for example, our <script> tag will not arrive as such to the client but as &lt;script&gt;. Wrapping the string in a Markup object tells Jinja2 that this string should not be escaped.

So now that we have a wrapper we need to hook it up with Jinja2 so that our templates can call it (file app/__init__.py):

from momentjs import momentjs
app.jinja_env.globals['momentjs'] = momentjs

This just tells Jinja2 to expose our class as a global variable to all templates.

Now we are ready to modify our templates. we have two places in our application where we display dates and times. One is in the user profile page, where we show the "last seen" time. For this timestamp we will use the calendar() formatting (file app/templates/user.html):

{% if user.last_seen %}
<p><em>Last seen: {{momentjs(user.last_seen).calendar()}}</em></p>
{% endif %}

The second place is in the post sub-template, which is invoked from the index, user and search pages. In posts we will use the fromNow() formatting, because the exact time of a post isn't as important as how long ago it was made. Since we abstracted the rendering of a post into the sub-template we now need to make this change in just one place to affect all the pages that render posts (file app/templates/post.html):

<p><a href="{{url_for('user', nickname = post.author.nickname)}}">{{post.author.nickname}}</a> said {{momentjs(post.timestamp).fromNow()}}:</p>
<p><strong>{{post.body}}</strong></p>

And with these simple template changes we have solved all our timestamps issues. We didn't need to make a single change to our server code!

Final words

Without even noticing it, today we've made an important step towards making microblog accessible to international users with the change to render dates and times according to the client configured locale.

In the next installment of this series we are going to make our international users even happier, as we will enable microblog to render in multiple languages.

In the meantime, here is the download link for the application with the new moment.js integration:

Download microblog-0.13.zip.

Or if you prefer, you can find the code on github here.

Thank you for following my tutorials, I hope to see you in the next one.

Miguel

Origin: http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiii-dates-and-times