专注细节
努力进步

Flask-Dates and Times

The problem with timestamps

As shown in the previous articles, the now() call returns the correct time for my location, while the utcnow() call returns the time in the UTC time zone.

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

There is a more important problem with this approach. For users in different timezone it will be awfully difficult to figure out when a post was made if they see times in the PST timezone.

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 from in which we present users with a dropdown with list of timezones. Users should be ad to enter their timezone when they access the site for the first time, as part of their registration.

For security reasons web browsers will not allow us to get into the computers of our users to obtain this information.

As it 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 enbaled (no modern site will work without scripting), so this solution has potential.

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

  • The first approach would be to have the web browser somehow send the timezone information to the server 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 second 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.

These two approach are valid, but the second one has an advantage.The browser is best able to render times according to the system locale configuration. And another advantage: 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.

To use moment.js in our application we need to write a little bit of Javascript into our templates.

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.js in the /app/static/js, so that it can be served to clients as a static file.
Next, we add the reference to this libraray in our base template:

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

we can add this script in the templates that show timestamps. But instead of doing 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.

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 <script>. Wrapping the string in a Markup object tells Jinja2 that this string should not be escaped.

So now that we have a wapper we need to hook it up with Jinja2 so that our template can call it:

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

This just tell 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. The first is 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.

<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>
分享到:更多 ()