Это двенадцатая часть серии мега-учебника Flask, в которой я собираюсь рассказать вам, как работать с датами и временем таким образом, чтобы это работало для всех ваших пользователей, независимо от того, где они проживают.
Оглавление
Глава 12: Дата и время (Эта статья)
Глава 13: I18n и L10n
Глава 14: Ajax
Глава 15: Улучшенная структура приложения
Глава 16: Полнотекстовый поиск
Глава 17: Развертывание в Linux
Глава 18: Развертывание на Heroku
Глава 19: Развертывание в контейнерах Docker
Глава 20: Немного магии JavaScript
Глава 21: Уведомления пользователей
Глава 22: Фоновые задания
Глава 23: Интерфейсы прикладного программирования (API)
Один из аспектов моего приложения для ведения микроблогов, который я долгое время игнорировал, - это отображение дат и времени. До сих пор я просто позволял Python отображать объект datetime
в модели User
и даже не потрудился отобразить его в модели Post
. В этой главе вы узнаете, как работать с этими временными метками.
Ссылки на GitHub для этой главы: Browse, Zip, Diff.
Адский часовой пояс
Использование Python на сервере для отображения дат и времени, которые отображаются пользователям в их веб-браузерах, на самом деле не очень хорошая идея, потому что то, что сервер считает своим местным временем, не будет иметь смысла для пользователей, которые живут в другом часовом поясе.
Совершенно ясно, что сервер должен управлять временем, которое является согласованным и независимым от его собственного местоположения и местоположения пользователей. Если это приложение разрастется до такой степени, что потребуется несколько производственных серверов в разных регионах мира, я бы не хотел, чтобы каждый сервер записывал временные метки в базу данных в разных часовых поясах, потому что это сделало бы невозможной работу с этими временами. Поскольку UTC является наиболее используемым единым часовым поясом и поддерживается в классе datetime
, именно его я и собираюсь использовать.
В главе 4 вы видели, как создавать временные метки UTC для записей в блоге. В качестве напоминания, вот краткий пример, показывающий, как это было сделано.:
>>> from datetime import datetime, timezone
>>> str(datetime.now(timezone.utc))
'2023-11-19 19:05:51.288261+00:00'
Но с этим подходом связана важная проблема. Пользователям из разных мест будет ужасно сложно определить, когда была сделана публикация, если они будут видеть время в часовом поясе UTC. Им нужно было бы заранее знать, что время указано в UTC, чтобы они могли мысленно подогнать его к своему собственному часовому поясу. Представьте пользователя, скажем, в часовом поясе PDT на Западном побережье США, который публикует что-то в 15: 00 и сразу видит, что сообщение появляется в 10: 00 по времени UTC, или, если быть более точным, в 22: 00. Это будет очень запутанно.
Хотя стандартизация временных меток в соответствии с UTC имеет большой смысл с точки зрения сервера, это создает проблему удобства использования для пользователей. Цель этой главы - представить решение, которое сохраняет все временные метки, управляемые сервером, в часовом поясе UTC, не отталкивая пользователей.
Преобразование часовых поясов
Очевидным решением проблемы является преобразование всех временных меток из сохраненных единиц UTC в местное время каждого пользователя при их отображении. Это позволяет серверу продолжать использовать UTC для обеспечения согласованности, в то время как преобразование "на лету", адаптированное к каждому пользователю, решает проблему удобства использования. Сложная часть этого решения - знать местоположение каждого пользователя.
На многих веб-сайтах есть страница конфигурации, где пользователи могут указывать свой часовой пояс. Для этого мне потребуется добавить новую страницу с формой, в которой я представляю пользователям раскрывающийся список часовых поясов. При первом входе на сайт пользователей могут попросить ввести их часовой пояс в рамках регистрации.
Хотя это достойное решение, решающее проблему, немного странно просить пользователей вводить часть информации, которую они уже настроили в своей операционной системе. Кажется, было бы эффективнее, если бы я мог просто получить настройки часового пояса с их компьютеров.
Как выясняется, веб-браузер знает часовой пояс пользователя и предоставляет его через стандартные API JavaScript даты и времени. На самом деле есть два способа воспользоваться информацией о часовом поясе, доступной через JavaScript:
Подход "старой школы" заключался бы в том, чтобы веб-браузер каким-то образом отправлял информацию о часовом поясе на сервер, когда пользователь впервые входит в приложение. Это можно было бы сделать с помощью вызова Ajax или гораздо проще с помощью мета-тега обновления. Как только сервер узнает часовой пояс, он может сохранить его в сеансе пользователя или записать в таблицу users в базе данных, и с этого момента корректировать с его помощью все временные метки во время отображения шаблонов.
Подход "новой школы" заключается в том, чтобы ничего не менять на сервере и позволить преобразованию UTC в местный часовой пояс происходить в браузере с использованием JavaScript.
Оба варианта допустимы, но второй имеет большое преимущество. Знания часового пояса пользователя не всегда достаточно для представления дат и времени в формате, ожидаемом пользователем. Браузер также имеет доступ к конфигурации языкового стандарта системы, которая определяет такие параметры, как время утра / вечера в сравнении с 24-часовыми часами, формат отображения даты DD / MM / ГГГГ в сравнении с MM / DD / ГГГГ и многие другие культурные или региональные стили.
И если этого недостаточно, у подхода новой школы есть еще одно преимущество. Есть библиотека с открытым исходным кодом, которая выполняет всю эту работу!
Знакомство Moment.js и Flask-Moment
Moment.js это небольшая библиотека JavaScript с открытым исходным кодом, которая выводит отображение даты и времени на новый уровень, поскольку предоставляет все мыслимые варианты форматирования, а затем и некоторые другие. Некоторое время назад я создал Flask-Moment, небольшое расширение Flask, которое позволяет очень легко интегрировать moment.js в ваше приложение.
Итак, давайте начнем с установки Flask-Moment:
(venv) $ pip install flask-moment
Это расширение добавляется в приложение Flask обычным способом:
app/__init__.py: Пример Flask-Moment.
# ...
from flask_moment import Moment
app = Flask(__name__)
# ...
moment = Moment(app)
# ...
В отличие от других расширений, Flask-Moment работает вместе с moment.js, поэтому все шаблоны приложения должны включать эту библиотеку. Чтобы гарантировать, что эта библиотека всегда доступна, я собираюсь добавить ее в базовый шаблон. Это можно сделать двумя способами. Самый прямой способ - явно добавить тег <script>
, который импортирует библиотеку, но Flask-Moment упрощает задачу, предоставляя функцию moment.include_moment()
, которая генерирует тег <script>
:
app/templates/base.html: Включить moment.js в базовый шаблон.
...
{{ moment.include_moment() }}
</body>
</html>
В большинстве случаев библиотеки JavaScript, используемые приложением, включены в конец содержимого <body>
, где находится загрузочный JavaScript-код.
Использование Moment.js
Moment.js делает класс moment
доступным для браузера. Первым шагом для отображения временной метки является создание объекта этого класса, передающего желаемую временную метку в формате ISO 8601. Вот пример, запущенный в консоли JavaScript браузера:
t = moment('2021-06-28T21:45:23+00:00')
Если вы не знакомы со стандартным форматом даты и времени ISO 8601, этот формат выглядит следующим образом:
{yyyy}-{mm}-{dd}T{hh}:{mm}:{ss}{tz}
Я уже решил, что буду работать только с часовыми поясами UTC, поэтому последней частью всегда будет +00:00
или в некоторых случаях эквивалент Z
, который представляет UTC в стандарте ISO 8601.
В объекте moment
предусмотрено несколько методов для различных вариантов рендеринга. Ниже приведены некоторые из наиболее распространенных вариантов:
moment('2021-06-28T21:45:23+00:00').format('L')
'06/28/2021'
moment('2021-06-28T21:45:23+00:00').format('LL')
'June 28, 2021'
moment('2021-06-28T21:45:23+00:00').format('LLL')
'June 28, 2021 10:45 PM'
moment('2021-06-28T21:45:23+00:00').format('LLLL')
'Monday, June 28, 2021 10:45 PM'
moment('2021-06-28T21:45:23+00:00').format('dddd')
'Monday'
moment('2021-06-28T21:45:23+00:00').fromNow()
'2 years ago'
В этом примере создается объект moment, инициализированный 28 июня 2021 года в 21:45 по Гринвичу. Вы можете видеть, что все параметры, которые я пробовал выше, отображаются в UTC + 1, который является часовым поясом, настроенным на моем компьютере. Вы можете ввести вышеуказанные команды в консоли вашего браузера, убедившись, что в странице, на которой вы открываете консоль, включена moment.js. Вы можете сделать это в микроблоге, при условии, что вы внесли вышеуказанные изменения для включения moment.js, или также на https://momentjs.com/.
Обратите внимание, как разные методы создают разные представления. С помощью метода format()
вы управляете форматом выходных данных с помощью строки формата. Метод fromNow()
интересен тем, что он отображает временную метку по отношению к текущему времени, поэтому вы получаете выходные данные, такие как "минуту назад" или "через два часа" и т.д.
Если вы работали непосредственно в JavaScript, приведенные выше вызовы возвращают строку с отображаемой временной меткой. Затем вам предстоит вставить этот текст в нужное место на странице, что, к сожалению, требует работы с DOM. Расширение Flask-Moment значительно упрощает использование moment.js за счет включения в ваши шаблоны объекта moment
, аналогичного объекту JavaScript.
Давайте посмотрим на временную метку, которая отображается на странице профиля. Текущий шаблон user.html позволяет Python генерировать строковое представление времени. Теперь я могу отобразить эту временную метку с помощью Flask-Moment следующим образом:
app/templates/user.html: Отрисовка временной метки с помощью moment.js.
{% if user.last_seen %}
<p>Last seen on: {{ moment(user.last_seen).format('LLL') }}</p>
{% endif %}
Итак, как вы можете видеть, Flask-Moment использует синтаксис, аналогичный синтаксису библиотеки JavaScript, с одним отличием, заключающимся в том, что аргументом для moment()
теперь является объект Python datetime
, а не строка ISO 8601. Вызов moment()
, выполняемый из шаблона, автоматически генерирует необходимый код JavaScript для вставки отображаемой временной метки в нужное место DOM.
Второе место, где я могу воспользоваться преимуществами Flask-Moment, находится во вложенном шаблоне _post.html, который вызывается с главной страницы и страницы пользователя. В текущей версии шаблона каждому сообщению предшествует строка "username says:". Теперь я могу добавить временную метку, отображаемую с помощью fromNow()
:
app/templates/_post.html: Отрисовка временной метки во вложенном шаблоне post.
<a href="{{ url_for('user', username=post.author.username) }}">
{{ post.author.username }}
</a>
said {{ moment(post.timestamp).fromNow() }}:
<br>
{{ post.body }}
Ниже вы можете увидеть, как выглядят обе эти временные метки при рендеринге с помощью Flask-Moment и moment.js: