Как стать автором
Обновить

Мега-Учебник Flask, Часть XII: Даты и время (издание 2018)

Время на прочтение8 мин
Количество просмотров25K

Мега-Учебник Flask, Часть XII: Даты и время (издание 2018)


Miguel Grinberg




Туда Сюда


Это двенадцатая часть серии Мега-Учебник Flask, в которой я расскажу вам, как работать с датой и временем таким образом, что бы пользователи, не зависели от того, в каком часовом поясе они находятся.


Для справки ниже приведен список статей этой серии.



Примечание 1: Если вы ищете старые версии данного курса, это здесь.


Примечание 2: Если вдруг Вы захотели бы выступить в поддержку моей(Мигеля) работы, или просто не имеете терпения дожидаться статьи неделю, я (Мигель Гринберг)предлагаю полную версию данного руководства(на английском языке) в виде электронной книги или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.


Один из аспектов моего приложения Microblog, который я игнорировал в течение длительного времени — отображение даты и времени. До сих пор я просто позволял Python отобразить объект datetime в модели User и полностью проигнорировал это в модели Post.


Ссылки GitHub для этой главы: Browse, Zip, Diff.


Адский часовой пояс


Использование функционала Python на сервере для отображения даты и времени, которые отображены пользователям в их веб-браузерах, не очень хорошая идея. Рассмотрим следующий пример. Я пишу это в 16:06 28 сентября 2017. Мой часовой пояс в то время, когда я пишу это, — это PDT (или UTC-7, если хотите). В интерпретаторе Python я получаю следующее:


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

Вызов datetime.now() возвращает правильное время для моего местоположения, а вызов datetime.utcnow() возвращает время в часовом поясе UTC. Если бы я мог попросить людей, живущих в разных частях света, запустить этот код в то же время со мной, то функция datetime.now() вернула бы разные результаты для каждого человека, но datetime.utcnow() всегда будет возвращать одно значение в то же время, независимо от местоположения. Итак, какой вариант, по вашему мнению, лучше использовать в веб-приложении, которое, скорее всего, будет доступно пользователям по всему миру?


Очевидно, что сервер должен управлять временем, которое не зависит от местоположения. Если это приложение будет развиваться и появится необходимость использования нескольких production серверов в разных регионах по всему миру, я бы не хотел, чтобы каждый сервер записывал временные метки в базу данных в соответствии разными часовыми поясами, потому что это затруднит работу с этими данными. Поскольку UTC является наиболее используемым единообразным часовым поясом и поддерживается в классе datetime, это то, что надо! И я буду это использовать.


Но в этом варианте появляется одна важная проблема. Для пользователей в разных часовых поясах будет сложно определить, когда реально была сделана запись, если они видят время в часовом поясе UTC. Им нужно заранее понять, что время отображается в UTC и надо суметь мысленно настроить время на свой часовой пояс. Представьте себе пользователя в часовом поясе PDT, который отправляет что-то в 3:00 вечера, и сразу же видит, что сообщение появляется с временем UTC в 22:00 или, точнее, 22:00. Это будет выглядеть запутанно.


Хотя стандартизация временных меток UTC имеет большой смысл с точки зрения сервера, но создает сложности с удобством использования для пользователей. Цель этой главы — решить эту проблему, сохранив все временные метки, управляемые на сервере в UTC.


Конверсия часовых поясов


Очевидным решением этой проблемы является преобразование всех временных меток из UTC в локальное время каждого пользователя. Это позволяет серверу продолжать использовать UTC для согласованности, в то время как преобразование «на лету», адаптированное для каждого пользователя, решает проблему удобства. Сложность этого решения заключается в определении местоположение каждого пользователя.


На многих веб-сайтах есть страница конфигурации, где пользователи могут указывать свой часовой пояс. Это потребует от меня добавления новой страницы с формой, в которой я представляю пользователям выпадающий список со списком часовых поясов. Пользователям будет предложено ввести свой часовой пояс, когда они впервые обращаются к сайту, в рамках их регистрации.


Хотя это достойное решение, которое решает проблему, немного странно просить пользователей ввести часть информации, которую они уже настроили в своей операционной системе. Кажется, было бы более эффективно, если бы я мог просто узнать настройку часового пояса у локального компьютера.


Как выясняется, веб-браузер знает часовой пояс пользователя и предоставляет его через стандартные API JavaScript. На самом деле существует два способа использовать информацию о часовом поясе, доступную через JavaScript:


  • Подход «старой школы» заключался бы в том, чтобы веб-браузер каким-то образом отправлял информацию о часовом поясе на сервер, когда пользователь впервые регистрируется в приложении. Это можно сделать с помощью вызова Ajax или, что еще проще, с тегом meta refresh. Как только сервер определил часовой пояс, он может сохранить его в сеансе или записать его в запись пользователя в базе данных, а затем настроить все временные метки во время отображения шаблонов.


  • Подход «новой школы» состоял бы в том, чтобы не изменить ситуацию на сервере и позволить конвертировать с UTC в локальный часовой пояс на клиенте, используя JavaScript.
    Оба варианта рабочие, но второй имеет большое преимущество. Знать часовой пояс не всегда достаточно, чтобы представить дату и время в ожидаемом пользователем формате. Браузер также имеет доступ к конфигурации локали системы, которая определяет такие вещи, как AM/PM и 24-часовой формат, DD/MM/YYYY против MM/DD/YYYY и многие другие культурные или региональные стили.



И если и этого недостаточно, есть ещё одно преимущество в пользу «новой школы». Существует библиотека с открытым исходным кодом, которая делает все это!


Знакомство с 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 в базовый шаблон.

...

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

Блок scripts, который я здесь добавил, представляет собой еще один, экспортируемый базовым шаблоном Flask-Bootstrap. Это место, в которое должны быть включены импорт(ы) JavaScript. Этот блок отличается от предыдущего тем, что он уже поставляется с некоторым содержимым, определенным в базовом шаблоне. Все, что я хочу сделать, это добавить библиотеку moment.js, не теряя базового содержимого. И это достигается с помощью инструкции super(), которая сохраняет контент из базового шаблона. Если вы определяете блок в своем шаблоне без использования super(), то любой контент, определенный для этого блока в базовом шаблоне, будет потерян.


Использование Moment.js


Moment.js предоставляет браузеру доступный moment class. Первым шагом для создания временной метки является создание объекта этого класса, передачей требуемой метки времени в формате ISO 8601. Вот пример:


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

Если вы не знакомы со стандартным форматом ISO 8601 для дат и времени, формат выглядит следующим образом: {{ year }}-{{ month }}-{{ day }}T{{ hour }}:{{ minute }}:{{ second }}{{ timezone }}. Я уже решил, что я буду работать только с часовым поясом UTC, поэтому последняя часть всегда будет Z, которая представляет собой UTC в стандарте ISO 8601.


Объект moment предоставляет несколько методов для разных вариантов отображения. Ниже приведены некоторые из наиболее распространенных вариантов:


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"

В этом примере создается объект moment, инициализированный строкой 28 сентября 2017 года в 21:45 по UTC. Вы можете видеть, что все параметры, которые я пробовал выше, отображаются в UTC-7, который является часовым поясом, настроенным на моем компьютере. Вы можете ввести приведенные выше команды в консоль вашего браузера, убедившись, что страница, на которой вы открываете консоль, содержит moment.js. Вы можете сделать это в микроблоге, если вы внесли изменения выше, включив moment.js, а также на https://momentjs.com/.


Обратите внимание, как разные методы создают разные представления. При помощи format() вы управляете форматом вывода с помощью строки имени формата, аналогичной функции strftime из Python. Методы fromNow() и calendar() интересны тем, что они отображают временную метку по отношению к текущему времени, поэтому вы получаете строку вывода, такую как «минута назад» или «через два часа» и т.д.


Если бы вы работали непосредственно в JavaScript, приведенные выше вызовы возвратили строку с отображением временной метки. Значит для добавления этого текста в нужное место на странице, требуется использование 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 и moment.js находится в _post.html sub-шаблоне, который вызывается из индексной и пользовательской страниц. В текущей версии шаблона каждому сообщению предшествовала строка "username says:". Теперь я могу добавить временную метку с помощью fromnow():


app/templates/_post.html: Отметка времени субшаблоне сообщения.

            <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:



P.S. от переводчика


Если вам вдруг требуется, как и мне, отобразить дату на языке отличающемся от английского, например русском. Расширение flask-moment имеет для этой цели в своем арсенале метод lang().



app/templates/base.html: Добавил в блок moment.js в базовом шаблоне метод lang.

...

{% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}
    {{ moment.lang('ru') }}     <!-- Я добавил эту строку -->
{% endblock %}

Туда Сюда

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии1

Публикации

Истории

Работа

Data Scientist
78 вакансий
Python разработчик
121 вакансия

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань