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

  • #51 Jonathan Pepin said 2014-12-15T19:20:06Z

    Hey Miguel,

    thanks for the tutorial, it's helping a lot in understand flask better!

    One question: Why aren't you using a context preprocessor for the momentjs function, instead of adding this in the app.jinja_env variable in init? http://flask.pocoo.org/docs/0.10/templating/#context-processors

  • #52 Miguel Grinberg said 2014-12-17T17:20:26Z

    @Jonathan: this article is almost two years old now. Back then I don't remember if context processors were available, I was not aware of that feature. In any case, between then and now I released my Flask-Moment extension (https://github.com/miguelgrinberg/Flask-Moment) which does make use of a context processor for the same purpose shown in this article.

  • #53 Theo said 2015-01-13T17:11:47Z

    Hey Miguel, great tutorial.

    One small issue on this post is that you seem to be missing the '.' in front of the 'momentjs' import statement.

    Keep up the great work!

  • #54 Miguel Grinberg said 2015-01-14T02:54:14Z

    @Theo: fixed. Thanks!

  • #55 Jacopo Notarstefano said 2015-01-22T09:31:02Z

    Hi Miguel,

    thanks again for your tutorial!

    A small heads up: it appears that moment.js stopped working in this page.

    Cheers, Jacopo Notarstefano

  • #56 Miguel Grinberg said 2015-01-22T19:59:06Z

    @Jacopo: can you describe the problem?

  • #57 Oliver Bienert said 2015-02-24T08:53:52Z

    Hello Miguel,

    This is a great tutorial, thank you!

    One small note: I can confirm Jacopo's problem, date rendering doesn't work on this page, all result cells above are empty. This is what firebug prints several times in the console:

    ReferenceError: moment is not defined document.write(moment("2012-12-31T23:55:13Z").format('LLLL'));

    etc.

    Tested in Firefox, 35.0.1, Ubuntu 14.04 The same with Chromium, Version 40.0.2214.111 Ubuntu 14.04

    Cheers Oliver

  • #58 Risinger said 2015-03-18T16:24:33Z

    I am trying to have a UTC date (timestamp without time zone) in a SQLAlchemy database display the time in the user's browser with the time in the their timezone, not UTC.

    Here is my python/Flask code : #First I query the SQLAlchemy database timeclockinfo = TimeClock.query.filter_by(parent_id=current_user.parent_id, user_id=current_user.user_id, closed=1).all() #Then I tuple the data child_patient = zip(timeclockinfo, user_names, visit_dates ) #I render the data with Flask return render_template('locationcheckinrpt.html', form=form, child_patient=child_patient, searchForm=searchForm) . . #In the template I have a date time field for the records rendered {% for times in child_time %} {{ times[0].checkintime.strftime('%Y/%m/%d @ %I:%M %p') }} {% endfor %}

    =======================

    Do you have on how to have the UTC 'times[0].checkintime' display in the browser users timezone and not UTC.

    I do have the User enter their time zone so I subtract the appropriate number of hours.

    But, I cannot hack through getting this to display adjusted for the different timezone.

  • #59 Miguel Grinberg said 2015-03-19T04:59:04Z

    @Risinger: the easiest is to use a javascript library such as moment.js to render the time on the fly in the browser.

  • #60 Kirk said 2015-07-18T18:55:15Z

    The issue with the empty dates has to do with newer versions on moment.js deprecating that the "moment" method. The easy way to fix it is to roll back to an older version of moment.js. I rolled back to 2.4.0, and it seemed to do the trick.

  • #61 Rich said 2016-04-25T12:41:50Z

    Kirk's comment regarding the version was a bit of a red herring, in my case. The library works as intended, I just wasn't loading it in the head, but rather at the bottom of the base document, hence triggering undefined errors as it hadn't loaded the library yet. Moving it to the head as Miguel has it in his download example works as intended using the current version of moment.js.

  • #62 Nick Kocharhook said 2019-09-09T10:39:51Z

    Quick question, Miguel. This page says "The reason is that Jinja2 escapes all strings by default, so for example, our tag will not arrive as such to the client but as <script>.". But the Jinja documentation says:

    "What is used depends on the application configuration. The default configuration is no automatic escaping" https://jinja.palletsprojects.com/en/2.10.x/templates/#html-escaping

    Am I right in interpreting these two statements as being contradictory?

  • #63 Miguel Grinberg said 2019-09-10T18:33:06Z

    @Nick: you are confusing two different things. HTML in templates is obviously not escaped by default, you just write the HTML and it is sent exactly like you write it to the browser. What I meant in this article is strings that are passed programmatically to the template, which are escaped by default, because not doing it could mean a security risk if the contents of the strings comes from an external entity that could be malicious.

Leave a Comment