2018-02-20T19:06:54Z

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:

moment('2017-09-28T21:45:23Z').format('L')
"09/28/2017"
moment('2017-09-28T21:45:23Z').format('LL')
"September 28, 2017"
moment('2017-09-28T21:45:23Z').format('LLL')
"September 28, 2017 2:45 PM"
moment('2017-09-28T21:45:23Z').format('LLLL')
"Thursday, September 28, 2017 2:45 PM"
moment('2017-09-28T21:45:23Z').format('dddd')
"Thursday"
moment('2017-09-28T21:45:23Z').fromNow()
"7 hours ago"
moment('2017-09-28T21:45:23Z').calendar()
"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 }}
                </a>
                said {{ moment(post.timestamp).fromNow() }}:
                <br>
                {{ post.body }}

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

Flask-Moment

49 comments

  • #26 Miguel Grinberg said 2018-08-25T10:48:57Z

    @Kenny: something that is supposed to be a datetime is a string in your application. Check the stack trace for the location in the code where this problem happens, and go from there to figure out where you have a date as a string instead of a datetime object.

  • #27 Oleg Kovalenko said 2018-08-27T22:54:31Z

    Hi Miguel, It's been really fun going through your tutorial. It is really amazing how actively you get back to your followers. Thank you. One thing I didn't like is making a "_idiom.html" for every reusable html idiom, like you did for "_post.html". I would rather group these idioms as macros in files that I import anywhere I want to use them. How can I adapt your flask-moment code in this chapter to my "macros.html" file? It doesn't extend "base.html" so there is no "{% block scripts %}" to add "moment.include_moment()" to. I tried making up the "<script>" tags like you mentioned, but it fails to recognize "moment" and I get a Jinja2 error. I can't find anything relevant yet on momentjs.com and I've tried a few of their examples. I figured you might know. Cheers and thanks, Oleg

  • #28 Miguel Grinberg said 2018-08-29T22:47:02Z

    @Oleg: Have you seen how to create macros in Jinja2? http://jinja.pocoo.org/docs/2.10/templates/#macros

  • #29 Oleg Kovalenko said 2018-09-03T06:06:20Z

    Miguel, I've got it working. I had to do some research to understand how Flask determines Jinja2 environments (which Flask for some reason calls contexts). By default, imported templates do not receive the variables that render_template normally provides implicitly, including the _moment class instance in your Flask-Moment extension. To use something like momentjs in a Jinja2 template of macros, the Flask documentation recommends using "{% import ___ with ___ %}", but this would require a lot of redundant typing for no benefit of clarity. Fortunately, the Jinja2 API directly offers a reasonable solution: you can add a global variable to all Jinja2 environments after initializing the "app" variable in "__init__.py" by adding on the next line "app.jinja_env.globals['moment'] = _moment" as well as importing the "_moment" class from your Flask-Moment extension "import from flask_moment Moment, _moment" Well, that was fun! Maybe this comment might even help someone. Thanks again, Oleg

  • #30 AR said 2018-09-24T15:13:17Z

    Hi Miguel, I have done all the following steps - 1. pip install flask-moment 2. import Moment in __init.py__ and created a moment instance 3.Directly included moment.js in the template using <script>. I have not used {{ moment.include_moment() }} because i am not using flask-bootstrap. I have tested in chrome console of my webpage that direct moment('2017-09-28T21:45:23Z').format('L') is giving output as "09/29/2017" Problem -- in template <p> {{ moment(user.date).format('LLL') }}</p> returning blank. what could be the reason?

  • #31 Miguel Grinberg said 2018-09-25T18:15:38Z

    @AR: include_moment is required, it is not dependent on bootstrap.

  • #32 AR said 2018-09-26T07:06:05Z

    I have included as shown below -- {% block scripts %} {{ moment.include_moment() }} {% endblock %} CDN link and js code appears when i check page source. But still, <p> {{ moment(user.date).format('LLL') }}</p> returns blank. Moment works in chrome console of my web page.

  • #33 Miguel Grinberg said 2018-09-26T08:46:51Z

    @AR: Check the browser's console for errors. It does not look like you are doing this right, see the README page in the GitHub repository for how to call include_moment() when you use a locally hosted moment.js. Also note that you need jquery in your page for this extension to work.

  • #34 Drew said 2018-10-24T03:58:52Z

    @ Amirhossein, Tung This is a little late but if anyone else is having the issue with the dates not appearing, I fixed this by noticing I was missing the scripts block in my /templates/bootstrap/base.html file. Because the /templates/base.html inherits from this file, it needs this to be in the parent file.

  • #35 AR said 2018-11-11T09:06:12Z

    Hi Miguel, I got it working, had to shift the block {% block scripts %} {{ moment.include_moment() }} {% endblock %} after jquery link.

  • #36 Daniel BenDavid said 2018-12-04T21:57:16Z

    Hi Miguel, You mentioned in this chapter the Browser's console. Can you please elaborate how to inspect the current page and perhaps basic dev tools? Thanks, Daniel

  • #37 Miguel Grinberg said 2018-12-04T22:45:24Z

    @Daniel: How to access the developer tools depends on which browser you use. With Chrome you can right click anywhere on the page and then select the "Inspect" option.

  • #38 Rob said 2018-12-07T05:03:08Z

    Hi Miguel, this is great. Thank you. I have decided to not use Bootstrap and am using Bulma instead. Due to this I don't have access to the bootstrap block as follows in the base.html: {% block scripts %} {{ super() }} {{ moment.include_moment() }} {% endblock %} How can I get access to the moment.include_moment() function without bootstrap? Thank you

  • #39 Miguel Grinberg said 2018-12-07T10:21:41Z

    @Rob: The include_moment line just expands into a <script> block, you can put it in the <head> section of your page, for example.

  • #40 Gene said 2019-02-06T23:13:23Z

    Why do you have <span class="flask-moment" ... style="display: none"> I can't see the time in my web page , but the code is there How do I override this? Adding an CSS call doesn't fix this

  • #41 Miguel Grinberg said 2019-02-07T14:08:55Z

    @Gene: the Flask-Moment extension creates those elements hidden, and then once the date is correctly set makes them visible. For this to work you need to have JavaScript installed, and jQuery needs to be loaded. You may want to compare your code against mine to see what's different on your side. See the links to GitHub at the top of the article.

  • #42 Ricardo.W said 2019-05-09T00:36:41Z

    hey Miguel,i think that i cant find "app.run()" in this tutorial, why i can still run it?

  • #43 Miguel Grinberg said 2019-05-09T14:40:41Z

    @Ricardo: you can still use app.run(), but recent versions of Flask have the "flask run" command which works a bit better, so that is what I use in this latest version of the tutorial.

  • #44 Ricardo.W said 2019-05-10T09:15:12Z

    Thanks for your answer,but when i use app.run() in __init__.py, it didn't work. When i write app.run(processes=3)´╝îit still run well(it should be error in my windows i think). And when i search for "Cross database foreign key support",i read your answer again(XD),can i create foreign key cross database?i read the short document of flask-sqlalchemy but got nothing, i want to know more about it !

  • #45 Miguel Grinberg said 2019-05-10T11:54:32Z

    @Ricardo: you should call app.run from your main script, so microblog.py in the case of this application.

  • #46 Sascha said 2019-05-18T12:30:57Z

    I had an issue with the {{ super() }} statement in the scripts block: the content imported by that statement was "quoted" ("<" replaced by "&lt;" and such) and not actually interpreted as HTML, but just printed on the bottom of the page. I guess that is a security feature, to prevent code injections or so. What helped was to a write {{ super() | safe }}, I guess declaring it "save content", that can be interpreted without danger. I just wanted to add this, because it took me a while to find the solutions.

  • #47 Miguel Grinberg said 2019-05-18T17:55:44Z

    @Sancha: I'm confused, because super is supposed to return safe data directly. Unless this is something new that has change recently.

  • #48 Jose Duran said 2019-07-21T12:22:21Z

    Thanks a lot for the great tutorial Miguel! Just a heads up for anybody getting CORS errors in Chrome with Moment.js and not able to see the dates: browser extensions such as "Referer Control" and "Bypass Paywalls" might trigger those errors.

  • #49 Owen said 2019-07-30T15:29:25Z

    @Jose Duran, had a similar issue, my solution was to add the script to your head tag in the base.html file of your project manually, from this site https://cdnjs.com/libraries/moment.js/, mine was this;http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js

Leave a Comment