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

Posted by
on under

(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

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

63 comments
  • #1 Denis Fuenzalida said

    Thanks Miguel for this awesome series. I've learned about your tutorials last week and I've followed them all. Thanks for the effort you've put on them.

  • #2 Luis Villamarin said

    Miguel, Thanks for this great tutorial. It is certainly the best tutorial for flask I have seen. Thanks for taking the time to make so detail and complete. One last question, do you have any experience or suggestion on how to use flask and Google App Engine? I have tried different resources online but all of them are always missing something and I can't get it running to test my newly learned flask skills. Thanks in advance!

  • #3 Miguel Grinberg said

    @Luis: thanks. I haven't tried this myself, but think the tricky part is to adapt the database to the Google Datastore which is not SQL based so SQLAlchemy cannot be used. Another possibility is to use the Google Cloud SQL service for the database, which is currently free but will be paid after June 2013. This service is compatible with SQLAlchemy via the mysql+gaerdbms:// dialect. Good luck.

  • #4 Anders Vännman said

    Wounderful tutorial! Ive found it yesterday, and have started to do each in order. As an old developer in the pre-web-era I have lot of catch up to do. This is a great tutorial to see how the different frameworks fit together. In the old days we had just a few libraries :) tnx

  • #5 Martha Morrigan said

    Awesome tutorial.
    Can you post how to add tags at this microblog?
    Best wishes

  • #6 Miguel Grinberg said

    @Martha: I may do #hashtags in a future article, but it isn't really that complicated. You need a hashtag table, and then a many-to-many relationship between hashtags and posts. The relationship is populated from scanning posts for #<tag>.

  • #7 Echo Zhao said

    Thanks Miguel for this incomparable series.I've learned about this tutorials and I translate some to Chinese in http://www.oschina.net/translate/the-flask-mega-tutorial-part-xiii-dates-and-times .Hope this series can keep on.Thanks!

  • #8 Echo Zhao said

    Dear Miguel,I hope later article can teach us the Comet feature.Thanks

  • #9 Tian Siyuan said

    A minor error around:

    def __init__(self, timestamp):
    self.timestamp = timestamp
    
  • #10 Miguel Grinberg said

    @Tian: I corrected the indentation on that code. Thanks!

  • #11 Souvik said

    Hi Miguel, I am getting a "class momentjs has no attribute 'call' " error. Just to make sure I did not miss something I even tried to use your code base. Any suggestions on where I could be going wrong.

  • #12 Miguel Grinberg said

    @Souvik: did you set the Jinja2 globals to include momentjs? The error means that the momentjs variable isn't a callable object.

  • #13 Joe Wagner said

    Miguel, thanks much for the tutorial. I am getting the same error at Souvik. Is it possible that Jinja or Moment has changed something resulting in the error. Thanks again, Joe

  • #14 Miguel Grinberg said

    @Joe: can you show me the stack trace of the error?

  • #15 Kumar Arcot said

    Miguel,

    I get the same error too. I am running it under windows with venv. The stacktrace is as follows:

    AttributeError

    AttributeError: class momentjs has no attribute 'call'
    Traceback (most recent call last)

    File "G:\home\microblog\venv\lib\site-packages\flask\app.py", line 1701, in __call__
    
    return self.wsgi_app(environ, start_response)
    
    File "G:\home\microblog\venv\lib\site-packages\flask\app.py", line 1689, in wsgi_app
    
    response = self.make_response(self.handle_exception(e))
    
    File "G:\home\microblog\venv\lib\site-packages\flask\app.py", line 1687, in wsgi_app
    
    response = self.full_dispatch_request()
    
    File "G:\home\microblog\venv\lib\site-packages\flask\app.py", line 1360, in full_dispatch_request
    
    rv = self.handle_user_exception(e)
    
    File "G:\home\microblog\venv\lib\site-packages\flask\app.py", line 1358, in full_dispatch_request
    
    rv = self.dispatch_request()
    
    File "G:\home\microblog\venv\lib\site-packages\flask\app.py", line 1344, in dispatch_request
    
    return self.view_functions[rule.endpoint](**req.view_args)
    
    File "G:\home\microblog\venv\lib\site-packages\flask_login.py", line 496, in decorated_view
    
    return fn(*args, **kwargs)
    
    File "G:\home\microblog\app\views.py", line 71, in index
    
    posts = posts)
    
    File "G:\home\microblog\venv\lib\site-packages\flask\templating.py", line 125, in render_template
    
    context, ctx.app)
    
    File "G:\home\microblog\venv\lib\site-packages\flask\templating.py", line 107, in _render
    
    rv = template.render(context)
    
    File "G:\home\microblog\venv\lib\site-packages\jinja2\environment.py", line 970, in render
    
    return self.environment.handle_exception(exc_info, True)
    
    File "G:\home\microblog\venv\lib\site-packages\jinja2\environment.py", line 743, in handle_exception
    
    reraise(exc_type, exc_value, tb)
    
    File "G:\home\microblog\app\templates\index.html", line 2, in top-level template code
    
    {% extends "base.html" %}
    
    File "G:\home\microblog\app\templates\base.html", line 64, in top-level template code
    
    {% block content %}{% endblock %}
    
    File "G:\home\microblog\app\templates\index.html", line 27, in block "content"
    
    {% include 'post.html' %}
    
    File "G:\home\microblog\app\templates\post.html", line 6, in top-level template code
    
    <p>{{ _('%(nickname)s said %(when)s:', nickname = '<a href="%s">%s</a>' % (url_for('user', nickname = post.author.nickname), post.author.nickname), when = momentjs(post.timestamp).fromNow()) }}</p>
    
    AttributeError: class momentjs has no attribute '__call__'
    
  • #16 Kumar Arcot said

    Forgot to add that I didn't change anything in the code including the setting of the jinja2 globals:

    THINK /g/home/microblog (master)
    $ git status

    <h1>On branch master</h1> <h1>Untracked files:</h1> <h1>(use "git add <file>..." to include in what will be committed)</h1> <h1></h1> <h1>venv/</h1>

    nothing added to commit but untracked files present (use "git add" to track)

    $ git diff !$
    git diff app/init.py
    THINK /g/home/microblog (master)

  • #17 Bastian said

    First of all thanks for the great tutorial. Really helped me. I spotted a small error with momentjs class, you can't call it momentjs() for it is not a callable. It can be fixed by adding:

    def __call__(self, *args):
        return self.format(*args)
    

    cheers

  • #18 Miguel Grinberg said

    @Bastian: you are not correct. The "momentjs" item is a class, when you call it you are constructing a new object. This works fine for me:

    $ flask/bin/python
    Python 2.7.3 (default, Dec 18 2012, 13:50:09)
    [GCC 4.5.3] on cygwin
    Type "help", "copyright", "credits" or "license" for more information.

    from app import momentjs
    from datetime import datetime
    m = momentjs(datetime.now())
    print m.fromNow()

    <script> document.write(moment("2013-05-28T21:09:42 Z").fromNow()); </script>
  • #19 Miguel Grinberg said

    @Kumar: try the test in the comment above. If that works, then you may want to expose the expression "type(momentjs)" somewhere in the template, to see what you get there. Using Python 2.7.3 I get the expected type, which is "classobj".

  • #20 Kumar Arcot said

    I get the same result as your test from the command line. As I am new to python, and I tried many ways to expose the type(momentjs) expression and keep getting;

    jinja2.exceptions.UndefinedError
    UndefinedError: 'type' is undefined

    {{ _('%(typemj)s said ', typemj = '%s' % (type(momentjs)))}} I am on Python 2.66. Will upgrade to 2.7.X be the quick fix? Thanks

  • #21 Kumar Arcot said

    @Miguel: It may not be correct but @Bastian's suggestion works!

  • #22 bobwal said

    Hi @Miguel, I've been getting the same error too. @Bastian's suggestion also worked for me.

  • #23 Miguel Grinberg said

    @Kumar & @bobwal: There is clearly something I'm missing in my environment. I tested Python 2.7.x and 2.6.x with this and in both cases things work fine for me. Glad you are back on track, when I figure this out I'll report back with my findings.

  • #24 Vincent Chen said

    Hi Miguel, thanks for you tutorial! This really helps me a lot as a quick start to web development, since I was doing desktop application development before and web is so different.
    The problem for me is I've followed all the steps from lesson 1 and all of your code works find so far. But when trying to run the code from this article it always get an error of 'AttributeError: class momentjs has no attribute 'call'. Don't know what happened and I've spend one hour on this and still can't figure it out. Any suggestions? BTW I'm using python 2.7

  • #25 Miguel Grinberg said

    @Vincent: See the comments above yours for a possible workaround to this problem. I haven't determined exactly what the problem is, but will update the article once I do. Thanks!

Leave a Comment