The Flask Mega-Tutorial Part XII: Dates and Times

This is the twelfth installment of the Flask Mega-Tutorial series, in which I'm going to tell you how to work with dates and times in a way that works for all your users, regardless of where they reside.

For your reference, below is a list of the articles in this series.

Note 1: If you are looking for the legacy version of this tutorial, it's here.

Note 2: If you would like to support my work on this blog, or just don't have patience to wait for weekly articles, I am offering the complete version of this tutorial packaged as an ebook or a set of videos. For more information, visit courses.miguelgrinberg.com.

One of the aspects of my Microblog application that I have ignored for a long time is the display of dates and times. Until now, I've just let Python render the datetime object in the User model, and have completely ignored the one in the Post model.

The GitHub links for this chapter are: Browse, Zip, Diff.

Timezone Hell

Using Python on the server to render dates and times that are presented to users on their web browsers is really not a good idea. Consider the following example. I'm writing this at 4:06PM on September 28th, 2017. My timezone at the time I'm writing this is PDT (or UTC-7 if you prefer). Running in a Python interpreter I get the following:

>>> from datetime import datetime
>>> str(datetime.now())
'2017-09-28 16:06:30.439388'
>>> str(datetime.utcnow())
'2017-09-28 23:06:51.406499'

The datetime.now() call returns the correct time for my location, while the datetime.utcnow() call returns the time in the UTC time zone. If I could ask many people living in different parts of the world to run the above code all at that same time with me, the datetime.now() function will return different results for each person, but datetime.utcnow() will always return the same time, regardless of location. So which one do you think is better to use in a web application that will very likely have users located all over the world?

It is pretty clear that the server must manage times that are consistent and independent of location. If this application grows to the point of needing several production servers in different regions around the world, I would not want each server to write timestamps to the database in different timezones, because that would make working with these times impossible. Since UTC is the most used uniform timezone and is supported in the datetime class, that is what I'm going to use.

But there is an 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 UTC timezone. They would need to know in advance that the times are in UTC so that they can mentally adjust the times to their own timezone. Imagine a user in the PDT timezone that posts something at 3:00pm, and immediately sees that the post appears with a 10:00pm UTC time, or to be more exact 22:00. That is going to be very confusing.

While standardizing the timestamps to UTC makes a lot of sense from the server's perspective, this creates a usability problem for users. The goal of this chapter is to address this problem while keeping all the timestamps managed in the server in UTC.

Timezone Conversions

The obvious solution to the problem is to convert all timestamps from the stored UTC units to the local time of each user. This allows the server to continue using UTC for consistency, while an on-the-fly conversion tailored to each user solves the usability problem. The tricky part of this solution is to know the location of each user.

Many websites have a configuration page where users can specify their timezone. This would require me to add a new page with a form in which I present users with a dropdown with the list of timezones. Users can 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 the problem, it is a bit odd to ask users to enter a piece of information that they have already configured in their operating system. It seems it would be more efficient if I could just grab the timezone setting from their computers.

As it turns out, the web browser knows the user's timezone, and exposes it through the standard date and time JavaScript APIs. There are actually two ways to take advantage of the timezone information 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 application. 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 or write it to the user's entry in the database, and from then on adjust all timestamps with it at the time templates are rendered.
  • The "new school" approach would be to not change a thing in the server, and let the conversion from UTC to local timezone happen in the client, using JavaScript.

Both options are valid, but the second one has a big advantage. Knowing the timezone of the user isn't always enough to present dates and times in the format expected by the user. The browser has also access to the system locale configuration, which specifies things like AM/PM vs. 24 hour clock, DD/MM/YYYY vs. MM/DD/YYYY and many other cultural or regional styles.

And if that isn't enough, there is yet one more advantage for the new school approach. There is an open-source library that does all this work!

Introducing Moment.js and Flask-Moment

Moment.js is a small open-source JavaScript library that takes date and time rendering to another level, as it provides every imaginable formatting option, and then some. And a while ago I created Flask-Moment, a small Flask extension that makes it very easy to incorporate moment.js into your application.

So let's start by installing Flask-Moment:

(venv) $ pip install flask-moment

This extension is added to a Flask application in the usual way:

app/__init__.py: Flask-Moment instance.

# ...
from flask_moment import Moment

app = Flask(__name__)
# ...
moment = Moment(app)

Unlike other extensions, Flask-Moment works together with moment.js, so all templates of the application must include this library. To ensure that this library is always available, I'm going to add it in the base template. This can be done in two ways. The most direct way is to explicitly add a <script> tag that imports the library, but Flask-Moment makes it easier, by exposing a moment.include_moment() function that generates the <script> tag:

app/templates/base.html: Including moment.js in the base template.


{% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}
{% endblock %}

The scripts block that I added here is another block exported by Flask-Bootstrap's base template. This is the place where JavaScript imports are to be included. This block is different from previous ones in that it already comes with some content defined in the base template. All I want to do is add the moment.js library, without losing the base contents. And this is achieved with the super() statement, which preserves the content from the base template. If you define a block in your template without using super(), then any content defined for this block in the base template will be lost.

Using Moment.js

Moment.js makes a moment class available to the browser. The first step to render a timestamp is to create an object of this class, passing the desired timestamp in ISO 8601 format. Here is an example:

t = moment('2017-09-28T21:45:23Z')

If you are not familiar with the ISO 8601 standard format for dates and times, the format is as follows: {{ year }}-{{ month }}-{{ day }}T{{ hour }}:{{ minute }}:{{ second }}{{ timezone }}. I already decided that I was only going to work with UTC timezones, so the last part is always going to be Z, which represents UTC in the ISO 8601 standard.

The moment object provides several methods for different rendering options. Below are some of the most common options:

"September 28, 2017"
"September 28, 2017 2:45 PM"
"Thursday, September 28, 2017 2:45 PM"
"7 hours ago"
"Today at 2:45 PM"

This example creates a moment object initialized to September 28th 2017 at 9:45pm UTC. You can see that all the options I tried above are rendered in UTC-7, which is the timezone configured on my computer. You can enter the above commands in your browser's console, making sure the page on which you open the console has moment.js included. You can do it in microblog, as long as you made the changes above to include moment.js, or also on https://momentjs.com/.

Note how the different methods create different representations. With format() you control the format of the output with a format string, similar to the strftime function from Python. The fromNow() and calendar() methods are interesting because they render the timestamp in relation to the current time, so you get output such as "a minute ago" or "in two hours", etc.

If you were working directly in JavaScript, the above calls return a string that has the rendered timestamp. Then it is up to you to insert this text in the proper place on the page, which unfortunately requires some JavaScript to work with the DOM. The Flask-Moment extension greatly simplifies the use of moment.js by enabling a moment object similar to the JavaScript one in your templates.

Let's look at the timestamp that appears in the profile page. The current user.html template lets Python generate a string representation of the time. I can now render this timestamp using Flask-Moment as follows:

app/templates/user.html: Render timestamp with moment.js.

                {% if user.last_seen %}
                <p>Last seen on: {{ moment(user.last_seen).format('LLL') }}</p>
                {% endif %}

So as you can see, Flask-Moment uses a syntax that is similar to that of the JavaScript library, with one difference being that the argument to moment() is now a Python datetime object and not an ISO 8601 string. The moment() call issued from a template also automatically generates the required JavaScript code to insert the rendered timestamp in the proper place of the DOM.

The second place where I can take advantage of Flask-Moment and moment.js is in the _post.html sub-template, which is invoked from the index and user pages. In the current version of the template, each post preceded with a "username says:" line. Now I can add a timestamp rendered with fromNow():

app/templates/_post.html: Render timestamp in post sub-template.

                <a href="{{ url_for('user', username=post.author.username) }}">
                    {{ post.author.username }}
                said {{ moment(post.timestamp).fromNow() }}:
                {{ post.body }}

Below you can see how both these timestamps look when rendered with Flask-Moment and moment.js:



  • #1 Jozef M H Dassen said 2018-02-21T15:40:34Z

    Wow this looks pretty bad !! I get all my times in AM/PM which nobody here understands. Why does moment not pick up the computers locale ?? And if it can not, how can I set it manually or get a 24 hr time format?? Looks like Flask-moment is pretty limited or not well documented......

  • #2 Miguel Grinberg said 2018-02-21T20:31:48Z

    @Jozef: Flask-Moment has nothing to do with this, as it is just a wrapper around the Moment.js library. The documentation that you are looking for is that of Moment.js, which is here: https://momentjs.com.

  • #3 Jozef M H Dassen said 2018-02-22T05:03:44Z

    OK, Miguel, I found usable information under Display - Format in the moment doc. Still, going for AM/PM by default looks bad. My locale definitely displays 24 hr format, so to have something running on it displaying AM/PM is just not right.

  • #4 Miguel Grinberg said 2018-02-22T07:53:24Z

    @Jozef: I feel you are complaining at the wrong place. I get it that you don't like the output, but what can I do? Moment.js is not my project. Maybe you should wait until the next chapter of this tutorial is published, which discusses internationalization and localization, and see if that works for you. Or else go to the moment.js page and select the locale that corresponds to your place to see if that fixes things for you.

  • #5 Serhiy said 2018-02-23T14:01:07Z

    @Jozef: Hi, Jozef. If you wish to use your locale date time format you have to put the corresponding command in <script> section. For example: English(United Kingdom) - moment.locale('en-gb'); Ukrainian - moment.locale('uk') Korean - moment.locale('ko') Croatian - moment.locale('hr') and so on (go by the Miguel's link for all supported locale formats). Put it like this: {% block scripts %} {{ super() }} {{ moment.include_moment() }} {{ moment.locale(...) }} {% endblock %}

  • #6 spmsh said 2018-03-08T08:02:57Z

    Hi Miguel, Great tutorial thanks a lot for all of this. I've followed most of your chapters to build my app, and I have used my db to store utc timezone db.Column(db.DateTime, index=True, default=datetime.utcnow). My problem is that it's not recording the timezone information in the datetime, but only the time itself (e.g., 2018-03-07 08:07:35.899000), there's no 'Z' at the end, hence, moment.js is not working because it doesn't recognized the timezone from the db. Do you know why the timezone info are not stored in the db ? I followed all the steps from your chapter 4, and I don't know what I'm missing now. Thanks.

  • #7 Miguel Grinberg said 2018-03-08T18:07:18Z

    @spmsh: The way I do this is by convention. I only write times in UTC to the database, so I don't really need to store the timezone, because by my own convention, it is always UTC. This goes well with the concept of "naive" datetime objects in Python (you can read about it in the Python docs, these are timestamps that do not have a timezone associated with them). When you retrieve one of these timestamps from the database, you may need to add the timezone, if the consumer requires it. In the case of moment, this is taken care for you if you use my Flask-Moment extension. If you are using moment.js directly, you will need to render the datetime object as iso8601 and append the 'Z' at the end.

  • #8 Ventura said 2018-03-09T23:47:49Z

    Hello Miguel, how can we do to add a specific datetime in the database? not using 'utcnow', but a string in the format "2018-04-31T22:00:00Z". I donĀ“t know how to prepare the database model for it, as well as the manual addition using flask-sqalchemy. Could you please help me?

  • #9 Miguel Grinberg said 2018-03-10T00:12:35Z

    @Ventura: create a string column with the appropriate length and write your ISO8601 date manually to it.

  • #10 Alex said 2018-03-16T10:42:17Z

    Hi Miguel. Thank you for so nice tutorial about Flask. It is the best what I saw. I'm trying to make my own app by your tutorial and till now everything is clear for me and it is working like I want. Thanks a lot. @Jozef: I think this link will help you to get the date format which you need http://momentjs.com/docs/#/displaying/

  • #11 Ghouse said 2018-03-20T07:12:47Z

    Hi miguel, I've created my own application by learning from your blog and ebook. I've a requirement to display graph such as x-axis contains job name, y-axis contains how much time it takes to complete. I've have a job1 start time and completed time as below. Now i want to display graph as job1 takes 8days 6hours 43mins in web UI. how can i achieve this.Please guide me with right path. job1 start time : 2018-03-04T22:30:51.000-0800 job1 completed time : 2018-03-13T06:14:03.000-0700 Example: 1. Job1 takes 8days 6hours 43mins 2. Job2 takes 5hours 3mins 3. job3 takes 5mins

  • #12 Miguel Grinberg said 2018-03-20T17:51:52Z

    @Ghouse: displaying durations is currently not supported by Flask-Moment, but the Moment.js library does support it. If you calculate your duration in seconds in the server, then in the client you can do "moment.duration(s, "seconds").humanize()" to render a human friendly representation. See https://momentjs.com/docs/#/durations/ for reference on duration support in moment.js.

  • #13 reem said 2018-03-25T06:42:55Z

    First of all thank you for the hard work! secondly, I went to read the documentation on moment.js and I found this: var moment = require('moment'); moment.locale('fr'); moment(1316116057189).fromNow(); how can I implement this using flask-moment? I tried adding .locale() to my moment object but it didn't work Thank you again :D

  • #14 Miguel Grinberg said 2018-03-25T15:18:13Z

    @reem: see the documentation: https://github.com/miguelgrinberg/Flask-Moment#internationalization

  • #15 Mark said 2018-04-16T23:04:33Z

    Hi Miguel, For security reasons, I need to use a local copy of moment.js in my app. I've been combing the Flask-Moment code, as well as the Flask-Bootstrap code, but I can't seem to find where to specify the local path. I have local copies of Bootstrap and jQuery loaded properly, but I don't see a similar way to specify Moment. Can you provide some guidance? Thanks!

  • #16 Miguel Grinberg said 2018-04-16T23:24:52Z

    @Mark: you can use the following: {{ moment.include_moment(local_js='your-local-moment-js-path-here') }} Hope this helps!

  • #17 Philip said 2018-05-03T09:33:42Z

    Hi Miguel, I'm loving this course!..thank you... I have a question. in this section where you say and I quote "And this is achieved with the super() statement, which preserves the content from the base template. " . Which base template do you refer to?.. the Bootstrap base template or the one in the app directory?

  • #18 Miguel Grinberg said 2018-05-03T18:59:11Z

    @Philip: it's the one that is mentioned in the "extends" clause at the top. In this case, it refers to the base template from the application. But this base template in turn also uses super() to preserve elements that are defined in Flask-Bootstrap's base template.

Leave a Comment