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

83 comments

  • #51 Miguel Grinberg said 2019-10-22T15:29:32Z

    @Piotr: you have an error in your HTML template that is indirectly affecting your declaration. Compare your template carefully against mine to find the mistake.

  • #52 James said 2019-11-11T02:40:33Z

    If you're following along on MacOS and using Safari, you'll likely run into the same issue I did. Namely, dates simply don't display when using flask-moment. You can fix this by turning off the subresource integrity check: in base.html load moment with moment.include_moment(sri=False).

    Is this the safest / best thing to do? I don't know, honestly, but I can't figure out how to get it working otherwise. Weirdly, Chrome runs the app just fine without mucking with the SRI option.

  • #53 Miguel Grinberg said 2019-11-11T10:01:55Z

    @James: It appears there is some sort of new incompatibility that applies to Safari 13, which comes with the new Catalina upgrade.

  • #54 ADEBIYI TOMISIN said 2019-11-30T08:10:13Z

    Hi Miguel...thank you for this amazing platform. I'm new to coding and all. i cant seem to trace my current error. i keep getting 'dict object' has no attribute 'timestamp'

  • #55 Miguel Grinberg said 2019-12-01T00:15:16Z

    @ADEBIYI: look at the stack trace of your error, it will indicate the file and line number of the error. Then compare that part of the code against my version to find the mistake.

  • #56 Andre Maciel said 2019-12-29T14:45:31Z

    Hi Miguel, first of all, I'm really enjoying your tutorial, I'm learning a lot, also just bought the complete course.

    I'm struggling to make flask_moment to work, I've installed and set the config properly, also added the block script on Base.html.

    Then I open flask shell on the terminal and tried to run 'moment' there, the terminal response is: moment is not defined.

    And when I use flask run the profile page shows nothing on last seen - I've double-check in my DB and I have filled this field to my users, I see no reason to not work. I need help

  • #57 Miguel Grinberg said 2019-12-29T16:55:59Z

    @Andre: the moment extension works in template files, there is nothing you can do in a flask shell with it, since all the work happens in the web browser. My suggestion is that you download the working code from the link I offer at the top of the article, and use that as a guide in finding the mistake that you have made.

  • #58 Sean Heber said 2020-01-05T20:41:18Z

    I just want to reiterate what @James discovered - I too had set sri to False to get this to work on Safari/macOS:

    {{ moment.include_moment(sri=False) }}

    I admit I don't really understand the full implicates of this, but I noticed that the other scripts included from CDNs from the other extensions do not have an integrity attribute on them, either, so I guess they aren't using this sri protection by default? Perhaps Flask-Moment should default to False? I don't know - but in any case, it works now and I can move on. :P

  • #59 Miguel Grinberg said 2020-01-05T23:42:08Z

    @Sean: Just to clarify, this is only for MacOS Catalina and Safari 13. Older MacOS and Safari versions do work.

  • #60 NG said 2020-01-07T02:10:38Z

    Hi Miguel,

    First of all, thank you very much by this excellent tutorial and all the associated work !!! Saludos desde Argentina ! I am learning a lot from flask and python.

    In Microsoft Edge 44.18362.449.0 I have to disable SRI too as @SEan and @ James

    By the way, regarding @ADEBIYI: questions. I have the same issue 'dict object' has no attribute 'timestamp'

    its a problem when the Posts were hardcoded in the beginning of the tutorial, and the posts had not timestamp in the User profile. @ADEBIYI, you have to change the route in the routes.py

    Again, thank you so much Miguel !!!!!

  • #61 Brian said 2020-01-27T17:24:38Z

    Hi Miguel

    Your tutorial is amazing. The fact that you're covering so many important concepts like security, database migrations, and next localization... amazing. I'm learning a ton!

    One thing I found and by reading the commend re: Firefox above. I see this rendering fine in Chrome (the last seen date) but it's not rendering in safari. I've gone as far as to take your code from GitHub to make sure that I didn't make an error, but it's still happening. Nothing to worry about, just FYI. If you have any tips I'll take it, but again. Nothing urgent:

    Thank you again - simply amazing!

  • #62 Miguel Grinberg said 2020-01-30T10:09:39Z

    @Brian: See comment #52 and #58. Maybe that's what you are seeing.

  • #63 Paris Mollo said 2020-02-01T16:54:19Z

    I am amazed by how great is this tutorial series. You are very talented and have created a very completed course. Thank you!

  • #64 Dan said 2020-03-28T10:58:22Z

    Awesome tutorial and great attention to detail! Thank you very much for the detailed explanations! The dates do not work with Safari unfortunately, but other than that everything is awesome!

  • #65 Miguel Grinberg said 2020-03-28T13:58:25Z

    @Dan: That is a known problem with recent versions of Safari. See this issue for a workaround: https://github.com/miguelgrinberg/Flask-Moment/issues/54

  • #66 Aldric said 2020-04-23T08:18:33Z

    the span injected by flask moment gets assigned the style="/! display:none /" by default?

  • #67 Miguel Grinberg said 2020-04-23T09:55:41Z

    @Aldric: Does that present a problem? The JS code will display the span once it is ready.

  • #68 Eitan said 2020-05-05T10:46:02Z

    Has anyone tried this with Microsoft Edge? It seems that it blocks access to moment.js for security reasons because localhost isn't trusted enough.

  • #69 Miguel Grinberg said 2020-05-05T14:38:35Z

    @Eitan: what's the actual error message that you get?

  • #70 RR said 2020-06-10T11:43:38Z

    Trying to display moment in a bootstrap tooltip and getting some issues there.

    I would like to display the time by default as X days ago, but on hover show a tooltip with the actual date.

    I'm using Bootstrap tooltips for this, but it's not rendering at all.

    Is there anyway to store the date output by moment as a simple string that's created on page load?

    I know I can use jinja's {% set var = moment(user.last_seen).fromNow() %}

    but I can't figure out how to get just the final date as a string with the moment methods.

    Is there something I'm missing?

    Thanks, RR

  • #71 RR said 2020-06-10T12:11:18Z

    Regarding my previous issue with displaying the moment output in a tooltip.

    I've figured out the issue. It's the --- style="display: none"

    If I change that to display:block in the console, everything displays fine.

    Is there any way to control that attribute?

    Thanks, RR

  • #72 RR said 2020-06-10T12:54:00Z

    Ok, I've made some progress on this but now running into even more confusing problems.

    I looked at the documentation for flask-moment and saw that I could create the variables also on the server side. So I tried to do that and have the following code:

    timestamp = moment.create(user.last_seen).format('L') timestamp = timestamp.replace('display: none', 'display: block') timestamp = str(timestamp)

    Now I do indeed get the tooltip to display right away, but ...

    Moment isn't actually formatting anything this way - I just see the following no matter what I pass to .format()

    2020-06-10T09:54:01Z

    Is this an unsolvable issue or am I not seeing something obvious?

  • #73 Miguel Grinberg said 2020-06-10T22:58:09Z

    @RR: the whole point of flask-moment is to make it easy to include dates and times in Jinja templates. You are trying to do this in a Bootstrap component. My recommendation is that you use the moment library in JavaScript directly for this. I think it'll be easier than figuring out how to get the Jinja side to work.

  • #74 Hassan said 2020-06-14T03:41:09Z

    Hello Miguel,

    I have modified your settings a bit: I am using Flask-Bootstrap4 instead of flask-bootstrap-3.3.x, I figured out and had responsive pages within my app. When I added Flask-Moment and used the {% block scripts %} {{ super() }} {{ moment.include_moment() }} {% endblock scripts %} (I like to explicitly close my blocks).

    When I resize the page to check the responsiveness of the toggle button, I find the button to open the menu but not to close it. This is a weird behaviour and was absent before the use of the block I mentioned above. I wonder why not to put the inclusion in the head like what you have mentioned on your git page. I know that you are using the super() in order not to overwrite the scripts block on flask-bootstrap base-template. I wonder why I am getting this "non-closing" toggle button when using Flask-Moment while using Flask-bootstrap4. Can you please help?

  • #75 Miguel Grinberg said 2020-06-15T15:46:38Z

    @Hassan: I can't really think of a reason why Flask-Moment would do this, and I'm also not familiar with the Flask-Bootstrap4 extension that you are using. If you want to put the include_moment() call in the head block that is totally fine, there shouldn't be any issue with that.

Leave a Comment