Мега-Учебник Flask, Часть 13: Дата и время

Это тринадцатая статья в серии, где я описываю свой опыт написания веб-приложения на Python с использованием микрофреймворка Flask.

Цель данного руководства — разработать довольно функциональное приложение-микроблог, которое я за полным отсутствием оригинальности решил назвать microblog.

Примечание касательно GitHub


Для тех кто не заметил, я не так давно перенес исходные коды microblog-а на github. Репозитарий расположен по следующему адресу:
https://github.com/miguelgrinberg/microblog
Для каждого шага этого руководства добавлены соответствующие тэги.

Проблема с timestamp

Одна особенность нашего приложения, которую мы игнорировали длительное время, это отображение даты и времени. 

До настоящего момента, мы доверяли python отображать метки времени, хранящиеся в наших объектах User и Post по собственному усмотрению, что на самом деле не является хорошим решением. 

Рассмотрим следующий пример. Я пишу это в 15:54, 31 декабря 2012 года. Мой часовой пояс PST (или UTC -8 если Вам так больше нравится). Запустив интерпретатор python, я получаю следующие результаты: 

>>> from datetime import datetime
>>> now = datetime.now()
>>> print now
2012-12-31 15:54:42.915204
>>> now = datetime.utcnow()
>>> print now
2012-12-31 23:55:13.635874

Вызов now() возвращает правильное время для моего часового пояса, в то время как utcnow() отображает время по Гринвичу.

Так какую функцию лучше использовать?


Если мы решим использовать now(), тогда все временные метки, которые мы планируем сохранять в нашу базу данных, будут зависеть от местоположения нашего сервера и это может породить некоторые проблемы.

Однажды может возникнуть необходимость перенести сервер в другой часовой пояс. И в этом случае все хранящиеся в базе данных временные метки нужно будет исправить в соответствии с новым локальным временем перед запуском сервера.

Но существует и более серьезная проблема, связанная с этим подходом. Для пользователей из разных часовых поясов будет сложно определить, когда именно был опубликован пост, если они будут видеть его в часовом поясе PST. Чтобы определиться, пользователи должны знать также что время указано относительно PST.

Очевидно, что это не самый оптимальный вариант, и именно поэтому когда мы создавали нашу базу данных, было принято решение хранить метки времени по Гринвичу.

Однако, приведя временные метки к некоторому стандарту, мы решили только первую проблему (с переносом сервера в другой часовой пояс), в то время как второй вопрос остался без решения — временные метки будут отображаться абсолютно всем пользователям по Гринвичу. 

Это по-прежнему может запутать пользователей. Представьте себе пользователя из часового пояса PST, который разместил запись в блоге около 15:00. Запись тут же появилась на главной странице, но в ней указано время создания 23:00.

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

Временные метки пользователя


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

Но как мы узнаем местоположение наших пользователей?

На многих сайтах есть страница настроек, где пользователь может указать свой часовой пояс. Для этого потребуется добавить новую страницу с формой, содержащей выпадающий список существующих часовых поясов. Частью регистрации станет ответ на вопрос о часовом поясе пользователя.
Несмотря на то, что данное решение является вполне работоспособным и решает поставленную задачу, оно представляется несколько избыточным — просить пользователя вводить информацию, которая уже однажды настроена и хранится у них в ОС. Кажется, было бы гораздо эффективнее, просто получить настройки часового пояса с компьютера пользователя. 

Однако, из соображений безопасности, браузеры не позволят нам получить доступ к системе пользователя, а следовательно, и получить необходимую информацию. Даже если бы это было технически возможно, нам бы нужно было знать где именно искать текущие установки для часовых поясов на Windows, Linux, Mac, iOS, и Android(и это не считая менее распространенные ОС).

Оказывается, браузер знает часовой пояс пользователя и делает его доступным через стандартный javascript интерфейс. В современном Web 2.0 мире вполне можно рассчитывать на включенный в браузере javascript (на практике, ни один современный сайт не будет работать корректно с отключенным javascript), так что это решение достойно рассмотрения.

У нас есть два варианта использования информации о часовом поясе, предоставляемой javascript:

  1. Традиционный (oldschool) подход — попросить браузер отправить нам информацию о часовом поясе пользователя, когда пользователь впервые заходит к нам на сайт. Это может быть реализовано при помощи Ajax или, что гораздо проще, при помощи тэга meta refresh. Как только сервер узнает часовой пояс он может сохранить эту информацию в сессии пользователя и откорректировать все временные метки в шаблоне во время его отрисовки.
  2. Современный (new-school) подход состоит в том, чтобы не привлекать к этому процессу сервер и позволить ему отправлять временные метки браузеру пользователя по Гринвичу. А преобразование времени в соответствии с часовым поясом пользователя будет происходить на клиенте при помощи javascript.


Оба варианта корректны, но у второго есть преимущество. У браузера есть больше возможностей в правильной отрисовке даты в соответствии с настройками системной локали пользователя. Такие подробности, как AM/PM(12-часовой) или 24-часовой формат времени используется, ДД/MM/ГГ или MM/ДД/ГГ формат даты предпочтителен и многие другие доступны браузеру и неизвестны серверу. 

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

Знакомьтесь, moment.js

Moment.js это компактная javascript библиотека с открытым исходным кодом, которая переводит отрисовку даты и времени на новый уровень. Она предоставляет всевозможные опции форматирования, и еще кое-что.

Для использования moment.js в нашем приложении, нам придется добавить совсем немного javascript в наши шаблоны. Начнем мы с создания объекта  moment из времени в формате ISO 8601. К примеру, используя время по Гринвичу из примера выше, мы создадим объект moment вот так:

moment("2012-12-31T23:55:13 Z")

 

Как только объект создан, он может быть преобразован в строку с огромным разнообразием форматов. Например, довольно многословное отображение в соответствии с системными настройками локали, может выглядеть так:

moment("2012-12-31T23:55:13 Z").format('LLLL');

А вот как система отобразит эту дату:

Tuesday, January 1 2013 1:55 AM

Вот еще несколько примеров той же метки времени отображенной в различных форматах:

ФорматРезультат
L01/01/2013
LLJanuary 1 2013
LLLJanuary 1 2013 1:55 AM
LLLLTuesday, January 1 2013 1:55 AM
ddddTuesday


Поддержка библиотекой различных опций на этом не заканчивается. В дополнение к format() библиотека предлагает  fromNow() и calendar(), с гораздо более дружественным отображением временных меток:

ФорматРезультат
fromNow()2 years ago
calendar()01/01/2013

Заметьте, что во всех представленных примерах, сервер отдает одно и то же время по Гринвичу, а необходимые преобразования производятся уже браузером пользователя.

И последняя часть магии javascript, которую мы пропустили, это сделать строку возвращенную методом объекта moment видимой на странице. Самый простой способ добиться этого — использовать фунцию javascript document.write , как показано ниже:

document.write(moment("2012-12-31T23:55:13 Z").format('LLLL'));

И хотя использование document.write очень просто и понятно, как способ генерации фрагмента HTML документа при помощи javascript, следует помнить, что эта реализация имеет некоторые ограничения. Наиболее значительное из них состоит в том, что document.write может использоваться только во время загрузки документа и не может использоваться для изменения документа, после окончания его загрузки. И, как следствие этого ограничения, данное решение не будет работать при загрузке данных через Ajax.

Интеграция moment.js


Есть несколько вещей, которые нам нужно сделать, чтобы использовать  moment.js в нашем приложении.

Первое, нужно поместить скачанную библиотеку moment.min.js в папку /app/static/js, так, что она будет отдаваться клиентам как статика(статичный файл).

Затем, следует добавить эту библиотеку в наш базовый шаблон (файлapp/templates/base.html):

<script src="/static/js/moment.min.js"></script>

Теперь мы можем добавить тэги <script> в шаблоны, которые отображают временные метки и дело будет сделано. Но вместо этого, мы создадим обертку для  moment.js , которую мы сможем использовать в шаблонах. Это сохранит нам время в будущем, если мы решим изменить код отображения временных меток, потому что достаточно будет изменить всего один файл.

Наша обертка будет представлять собой очень простой python класс (файл app/momentjs.py):

from jinja2 import Markup

class momentjs(object):
    def __init__(self, timestamp):
        self.timestamp = timestamp

    def render(self, format):
        return Markup("<script>\ndocument.write(moment(\"%s\").%s);\n</script>" % (self.timestamp.strftime("%Y-%m-%dT%H:%M:%S Z"), format))

    def format(self, fmt):
        return self.render("format(\"%s\")" % fmt)

    def calendar(self):
        return self.render("calendar()")

    def fromNow(self):
        return self.render("fromNow()")

Обратите внимание, что метод  render не возвращает непосредственно строку, вместо этого он экземпляр класса Markup, предоставляемого Jinja2, нашим шаблонизатором. Смысл в том, что Jinja2 по умолчанию экранирует все строки, так, <script> может попасть к клиенту в таком виде & lt;script& gt;. Вернув вместо строки экземпляр объекта Markup, мы сообщили Jinja2 что эту строку экранировать не нужно.

Теперь, когда у нас есть обертка, мы должны добавить её в Jinja2 чтобы можно было использовать её в наших шаблонах(файл app/__init__.py):

from momentjs import momentjs
app.jinja_env.globals['momentjs'] = momentjs

Этим мы позволили Jinja2 предоставлять наш класс как глобальную переменную во всех шаблонах.

Теперь мы готовы внести изменения в наши шаблоны. Есть два места в нашем приложении, где мы отображаем дату и время. Первое — страница профиля пользователя, где мы показывает время последнего посещения. Для отображения на этой странице мы применим форматирование calendar()  (файл app/templates/user.html):

{% if user.last_seen %}
<p><em>Last seen: {{momentjs(user.last_seen).calendar()}}</em></p>
{% endif %}

Второе место — шаблон поста, который используется на страницах index, user и search. В шаблоне поста мы применим форматирование fromNow(), т. к. точное время создания поста не так важно, как время прошедшее после создания поста. Т.к. мы выделили отрисовку поста в отдельный шаблон, теперь мы должны внести изменения только в одном месте, чтобы изменить все страницы отображающие посты (файл app/templates/post.html):

<p><a href="{{url_for('user', nickname = post.author.nickname)}}">{{post.author.nickname}}</a> said {{momentjs(post.timestamp).fromNow()}}:</p>
<p><strong>{{post.body}}</strong></p>

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

Заключение


Даже не заметив, сегодня мы сделали значительный шаг в сторону увеличения доступности нашего приложения для пользователей из всех уголков Сети, путем изменения отображения даты в соответствии с настройками локали пользователя.

В следующей статье этой серии, мы постараемся порадовать наших иностранных пользователей еще больше, т. к. мы включим поддержку нескольких языков. 

Скачать: microblog-0.13.zip.
Или, если Вам так больше нравится, вы можете найти исходный код на GitHub.

Miguel

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.