2013-01-01T18:58:58Z

The Flask Mega-Tutorial, Part XIII: Dates and Times

(Great news! There is a new version of this tutorial!)

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.

NOTE: This article was revised in September 2014 to be in sync with current versions of Python and Flask.

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

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 the UTC time zone.

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 standardizing 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 grab the timezone setting from their computers.

For security reasons web browsers will not allow us to get into the computers of our users 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.

But 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 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 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 "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/YYYY vs. MM/DD/YYYY 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:13Z")

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:13Z").format('LLLL');

Below is how your system renders the above date:

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

FormatResult
L
LL
LLL
LLLL
dddd

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()
calendar()

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:13Z").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 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.

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

63 comments

  • #26 Vincent said 2013-05-31T23:33:26Z

    Sorry didn't realise there are few pages of comments :) Bastian's idea works for me as well, and I think the better way is to write: return self(args) instead of: return self.format(args) Since caller can call the calendar and fromNow functions as well

  • #27 Min said 2013-06-05T21:13:01Z

    Hi, did a fresh git clone on ubuntu 12.10 with python 2.7.3 and got the same stack trace as Kumar

    I guess Bastian is right - "Class instances are callable only when the class has a call() method" Got it working after adding that to momentjs.py

    def __call__(self, *args): return self.format(*args)
  • #28 Miguel Grinberg said 2013-06-05T22:16:02Z

    @Min: "momentjs" is not a class instance, it is a class object. From the same page on the Python docs: "A class instance is created by calling a class object". So I still don't understand what's going on and need to investigate some more.

  • #29 Olga said 2013-06-11T13:33:54Z

    One more problem...

    File "C:\Python27\myproject\petsreviews\app\templates\user.html", line 9, in block "content" {{momentjs(post.timestamp).fromNow()}} AttributeError: class momentjs has no attribute 'call'

    Do you have idea what could be wrong? Thank's

  • #30 Mauricio de Abreu Antunes said 2013-06-12T00:00:49Z

    Thanks, nice job. Your efforts are being awesome to me! :-)

  • #31 Miguel Grinberg said 2013-06-12T05:12:03Z

    @Olga, look at the comments above yours for this.

  • #32 Veronica said 2013-06-27T08:39:59Z

    Hi Miguel, I'm getting this error: AttributeError AttributeError: class momentjs has no attribute 'call'

    PD: I've downloaded your v.13, doesn't work too. Thank you

  • #33 Miguel Grinberg said 2013-06-28T03:56:04Z

    @Veronica: like Olga, a solution is in the comments above yours.

  • #34 Chris Larsen said 2013-06-29T14:56:19Z

    Love the tutorial, thanks!!

    Just a note that I ran into the AttributeError as well, exactly as Kumar's, I was getting no errors in the CLI, so I tried defining it in app/momentjs.py as Bastian recommended and it worked right away. FWIW my guess is there's a version issue somewhere.

    Thanks again for taking the time to do this tutorial.

    Chris

  • #35 Florian Sachs said 2013-07-06T13:37:15Z

    Hi Miguel,

    Thank you fort this awesome and inspiring tutorial - it's really fantasic!

    Using the class definition for the class 'momentjs' from your tutorial, i get an AttributeError: "AttributeError: class momentjs has no attribute 'call'" Making it a new style class by letting it inherit from object fixes the problem.

    I work on OS X 10.8.4 with Python 2.7.2.

    Thank you, florian

  • #36 Miguel Grinberg said 2013-07-06T17:27:10Z

    @Florian: thanks! I will update the code to match your solution.

  • #37 Rabbit said 2013-07-24T01:58:17Z

    This is a great tutorial. I'm using it to flesh out the Flask microblog tutorial. That project uses somewhat different technologies, though, which may be why I'm getting this error in momentjs.py:

    AttributeError: 'str' object has no attribute 'strftime'

    On line:

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

    Do have any idea why this is happening? I don't understand that line at all.

  • #38 Miguel Grinberg said 2013-07-24T16:18:57Z

    @Rabbit: Have is your "timestamp" defined? It should be a DateTime object, it appears you have ti as a string.

  • #39 Sam M said 2013-10-29T19:43:35Z

    Excellent article Miguel. Not sure if its only me, but I had to make some changes to get your code working. 1. strftime("%Y-%m-%dT%H:%M:%S Z") does not work with moment.js anymore, or with vanilla JS Date.Prototype on latest Chrome and Firefox. strftime("%Y/%m/%d %H:%M:%S %Z") worked however. (Python 2.7 running on Debian stable.) 2. The above method works with AJAX loading using the latest 1.x JQuery, since it supports JS evaluation for dynamically loaded content. But of course, document.write would need to be replaced with an alternative like $(element).append() method.

  • #40 Miguel Grinberg said 2013-10-30T04:19:15Z

    @Sam: You are correct on all accounts. Take a look at my new extension Flask-Moment which I've announced a few days ago, that's what I will be using going forward.

  • #41 Sam Redmond said 2013-11-10T13:45:28Z

    I think I may have found a mistake! After diligently following this tutorial (It's fantastic by the way), I am well on my way to making my own website. However, I think when you're dealing with moment.js, your time should be moment("2012-12-31T23:55:13Z") rather than moment("2012-12-31T23:55:13 Z").

    Notice that in one, there is a space between the :13 and the Z, whereas in the correct one there is not. I looked up the ISO standard at it appears that the Z is in fact supposed to follow immediately, without a space. The only big change, therefore, is in the momentjs.py file, where the formatting string passed to strftime should be "%Y-%m-%dT%H:%M:%SZ" instead of "%Y-%m-%dT%H:%M:%S Z".

    Again, thank you so much for this wonderful resource. I love it!

  • #42 Miguel Grinberg said 2013-11-10T18:48:25Z

    @Sam: If you are using the 2.x versions of moment.js you are correct, the Z needs to be right after the seconds. In fact my Flask-Moment extension formats dates that way. Back when I wrote this moment.js was at version 1.7, and at that time I could not make it work without the space.

  • #43 Joshua Grigonis said 2013-12-17T22:32:25Z

    Something seems to have broken around moment.js. On the fromNow() calls it always seems to come back with 'a few seconds ago' and on the calendar call it's giving an Invalid date.

  • #44 Joshua Grigonis said 2013-12-17T22:46:52Z

    It seems like post.timestamp is including decimal information on the seconds, and that moment doesn't like that, as it's not 8601 compliant. e.g. my post.timestamp looks like: 2013-12-17 19:08:12.325000

  • #45 Joshua Grigonis said 2013-12-17T22:56:49Z

    Removing the T seems to make it work on this line in momentjs.py return Markup("\ndocument.write(moment(\"%s\").%s);\n" % (self.timestamp.strftime("%Y-%m-%d %H:%M:%S Z"), format))

  • #46 Miguel Grinberg said 2013-12-18T05:16:44Z

    @Joshua: try version 1,7 of moment.js, that was the version I used when I wrote this article. The 2.x versions require a space between the seconds and the "Z" to be removed.

  • #47 Tim said 2013-12-19T18:57:46Z

    @Miguel: removing the space between seconds and "Z" is now showing the calendar. Thanks for that!

    Can't wait for the book!

  • #48 Peter said 2014-04-18T08:45:01Z

    Hello,

    i have a little problem. The dates are no shown after this tutorial. There are in the HTML-Source, but Chromium or Rekonq don't show it.

    What the matter? :)

    thanks Peter

  • #49 Miguel Grinberg said 2014-04-18T14:13:21Z

    @Peter: Maybe you have JavaScript disabled in your browser?

  • #50 Erick Rivas said 2014-11-12T17:17:43Z

    I debugged the "call" bug and it turns out to be caused by posts missing timestamps (so technically, corrupt data, which probably isn't all that uncommon).

    For now, this is my render function:

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

Leave a Comment