Miguel Grinberg
Эта статья является переводом шестой части нового издания учебника Мигеля Гринберга, выпуск которого автор планирует завершить в мае 2018.Прежний перевод давно утратил свою актуальность.
Я, со своей стороны, постараюсь не отставать с переводом.
Это шестой выпуск серии Flask Mega-Tutorial, в котором я расскажу вам, как создать страницу профиля пользователя.
Для справки ниже приведен список статей этой серии.
- Глава 1: Привет, мир!
- Глава 2: Шаблоны
- Глава 3: Веб-формы
- Глава 4: База данных
- Глава 5: Пользовательские логины
- Глава 6: Страница профиля и аватары (Эта статья)
- Глава 7: Обработка ошибок
- Глава 8: Подписчики, контакты и друзья
- Глава 9: Разбивка на страницы
- Глава 10: Поддержка электронной почты
- Глава 11: Реконструкция
- Глава 12: Дата и время
- Глава 13: I18n и L10n
- Глава 14: Ajax
- Глава 15: Улучшение структуры приложения
- Глава 16: Полнотекстовый поиск
- Глава 17: Развертывание в Linux
- Глава 18: Развертывание на Heroku
- Глава 19: Развертывание на Docker контейнерах
- Глава 20: Магия JavaScript
- Глава 21: Уведомления пользователей
- Глава 22: Фоновые задачи
- Глава 23: Интерфейсы прикладного программирования (API)
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
Эта глава будет посвящена добавлению страниц профилей пользователей в приложение. Страница профиля пользователя-это страница, на которой представлена информация о пользователе, как правило, введенной самими пользователями. Я покажу вам, как создавать страницы профилей для всех пользователей динамически, а затем я добавлю небольшой редактор профилей, который пользователи могут использовать для ввода своей информации.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Страница профиля пользователя
Чтобы создать страницу профиля пользователя, давайте сначала напишем новую функцию просмотра, которая будет отображаться в /user/<имя пользователя> URL.
@app.route('/user/<username>')
@login_required
def user(username):
user = User.query.filter_by(username=username).first_or_404()
posts = [
{'author': user, 'body': 'Test post #1'},
{'author': user, 'body': 'Test post #2'}
]
return render_template('user.html', user=user, posts=posts)
Декоратор @app.route
, который я использовал для объявления этой функции просмотра, немного отличается от предыдущих. В этом случае у меня есть динамический компонент, который обозначается как компонент URL-адреса <username>
, который заключен в треугольные скобки,<
и>
. Когда маршрут имеет динамический компонент, Flask принимает любой текст в этой части URL-адреса и вызывает функцию просмотра с фактическим текстом в качестве аргумента. Например, если клиентский браузер запрашивает URL: /user/susan, функция view
будет вызываться с именем пользователя в качестве аргумента, установленным на «susan»
. Эта функция просмотра будет доступна только для зарегистрированных пользователей, поэтому я добавил обработчик @login_required
из Flask-Login.
Реализация этой функции просмотра довольно проста. Сначала я пытаюсь загрузить пользователя из базы данных, используя запрос по имени пользователя. Вы уже видели, что запрос базы данных может быть выполнен путем вызова all()
, если вы хотите получить все результаты, или first()
, если вы хотите получить только первый результат или, если же ничего, соответствующего критериям поиска, не найдется, то эти методы вернут None. В этой функции представления я использую вариант first()
, называемый first_or_404(),
который работает точно так же, как first()
, когда есть результаты, но в случае отсутствия результатов автоматически обратно клиенту отправляется ошибка 404. Выполняя запрос таким образом, я не могу проверить, возвратился ли запрос пользователя, потому что, когда имя пользователя не существует в базе данных, функция не вернется, и вместо этого будет вызвано исключение 404.
Если запрос базы данных не вызывает ошибку 404, это означает, что был найден пользователь с указанным именем пользователя. Затем я инициализирую временный список сообщений для этого пользователя, наконец, создаю новый шаблон user.html
, которому передаю объект пользователя и список сообщений.
Шаблон user.html показан ниже:
{% extends "base.html" %}
{% block content %}
<h1>User: {{ user.username }}</h1>
<hr>
{% for post in posts %}
<p>
{{ post.author.username }} says: <b>{{ post.body }}</b>
</p>
{% endfor %}
{% endblock %}
Страница профиля завершена, но ссылка на нее не существует нигде на веб-сайте. Чтобы пользователям было проще проверить собственный профиль, я добавлю ссылку на него в панели навигации вверху:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
Единственное интересное изменение здесь — вызов url_for()
, который используется для создания ссылки на страницу профиля. Поскольку функция просмотра профиля пользователя принимает динамический аргумент, функция url_for()
получает значение для него как аргумент ключевого слова. Поскольку это ссылка, которая указывает на профиль пользователя в журнале, я могу использовать current_user
Flask-Login для генерации правильного URL-адреса.
Попробуйте! Нажав ссылку «Профиль» вверху страницы, вы попадете на свою страницу пользователя. На этом этапе нет ссылок, которые будут отображаться на странице профиля других пользователей, но если вы хотите получить доступ к этим страницам, вы можете ввести URL-адрес вручную в адресной строке браузера. Например, если у вас есть пользователь с именем «john», зарегистрированный в вашем приложении, вы можете просмотреть соответствующий профиль пользователя, введя http://localhost:5000/user/ john в адресной строке.
Аватары
Я уверен, вы согласитесь с тем, что страницы профиля, которые я только что построил, довольно скучны. Чтобы сделать их более интересными, я собираюсь добавить пользовательские аватары, но вместо того, чтобы иметь дело с возможно большой коллекцией загруженных изображений на сервере, я собираюсь использовать сервис Gravatar для предоставления изображений для всех пользователей.
Сервис Gravatar очень прост в использовании. Чтобы запросить изображение для данного пользователя, URL-адрес с форматом https://www.gravatar.com/avatar/, где <hash>
— хеш MD5 адреса электронной почты пользователя. Ниже вы можете увидеть, как получить URL-адрес Gravatar для пользователя с адресом электронной почты john@example.com
:
>>> from hashlib import md5
>>> 'https://www.gravatar.com/avatar/' + md5(b'john@example.com').hexdigest()
'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'
Если вы хотите увидеть фактический пример, можно использовать мой собственный URL-адрес Gravatar — https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35 ( 'https://www.gravatar.com/avatar/4f3699b436c12996ae54771200f21888' ). Вот что Gravatar возвращает для этого URL:
По умолчанию возвращенный размер изображения составляет 80x80 пикселей, но другой размер можно запросить, добавив аргумент s
в строку запроса URL. Например, чтобы получить мой собственный аватар в виде изображения размером 128x128, URL-адрес: https://www.gravatar.com/avatar/729e26a2a2c7ff24a71958d4aa4e5f35?s=128.
Другим интересным аргументом, который может быть передан Gravatar в качестве аргумента строки запроса, является d
, который определяет, какое изображение Gravatar предоставляет пользователям, у которых нет аватара, зарегистрированного в службе. Мой любимый называется «идентификатор», который возвращает приятный геометрический дизайн, который отличается для каждого письма. Например:
Обратите внимание, что некоторые расширения веб-браузера, такие как Ghostery, блокируют изображения Gravatar, поскольку они считают, что Automattic (владельцы Gravatar) могут определять, какие сайты вы посещаете, на основе запросов, которые они получают для вашего аватара. Если вы не видите аватары в своем браузере, скорее всего проблема в расширениях браузера.
Поскольку аватары связаны с пользователями, имеет смысл добавить логику, которая генерирует URL-адреса аватара для пользовательской модели.
from hashlib import md5
# ...
class User(UserMixin, db.Model):
# ...
def avatar(self, size):
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
digest, size)
Новый метод avatar()
класса User
возвращает URL-адрес изображения аватара пользователя, масштабируется до требуемого размера в пикселях. Для пользователей, у которых нет зарегистрированного аватара, будет создано изображение «идентификатор». Чтобы сгенерировать хэш MD5, я конвертирую адрес электронной почты в нижний регистр, поскольку этого требует Gravatar. Затем, конвертирую полученный hash-объект в шестнадцатеричную строку (метод .hexdigest()), прежде чем передавать ее хэш-функции.
Если вы заинтересованы в ознакомлении с другими вариантами, предлагаемыми службой Gravatar, посетите их сайт документации.
Следующий шаг — вставить изображения аватара в шаблон профиля пользователя:
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
<p>
{{ post.author.username }} says: <b>{{ post.body }}</b>
</p>
{% endfor %}
{% endblock %}
Большой плюс заключается в том, что пользовательский класс отвечает за возвращение URL-адресов аватаров.И если в какой-то день я решу, что аватары Gravatar не то, что я хочу, я могу просто переписать метод avatar()
, чтобы возвращать разные URL-адреса, и все шаблоны начнут показывать новые аватары автоматически.
У меня есть хороший большой аватар в верхней части страницы профиля пользователя, но на самом деле нет причин останавливаться на достигнутом. У меня есть несколько сообщений от пользователя внизу, в которых каждый может иметь маленький аватар. Конечно, для страницы профиля пользователя все сообщения будут иметь один и тот же аватар, но тогда я могу реализовать ту же функциональность на главной странице, а затем каждый пост будет украшен аватаром автора, и это будет выглядеть очень красиво.
Чтобы показать аватары для отдельных сообщений, мне просто нужно сделать еще одно небольшое изменение в шаблоне:
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
{% endfor %}
{% endblock %}
Вот так у Сьюзен, которой нет
(Прим. переводчика) А вот так с реальным аккаунтом Gravatar.
Использование sub-templates Jinja2
Я разработал страницу профиля пользователя, чтобы отображать сообщения, написанные пользователем, вместе со своими аватарами. Теперь я хочу, чтобы страница индекса также отображала сообщения с похожим расположением. Я мог бы просто закопипастить часть шаблона, которая касается рендеринга сообщения, но это не правильно, потому что позже, если я решу внести изменения в этот макет, мне придется помнить об обновлении обоих шаблонов.
Вместо этого я собираюсь сделать подшаблон, который просто отображает одно сообщение, а затем я буду ссылаться на него как с шаблонов user.html, так и с index.html. Для начала я могу создать подшаблон, с разметкой HTML для одного сообщения. Назову, пожалуй, это шаблонное нечто app/templates/_post.html. Префикс `_' — это просто соглашение об именах, которое помогает распознавать, какие файлы шаблонов являются подшаблонами.
<table>
<tr valign="top">
<td><img src="{{ post.author.avatar(36) }}"></td>
<td>{{ post.author.username }} says:<br>{{ post.body }}</td>
</tr>
</table>
Чтобы вызвать этот подшаблон из шаблона user.html, я использую оператор include
Jinja2:
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td><h1>User: {{ user.username }}</h1></td>
</tr>
</table>
<hr>
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
{% endblock %}
Страница index приложения на самом деле еще не сформирована, поэтому я пока не собираюсь добавлять эту функциональность.
Более интересные профили
Одна из проблем, с которой сталкиваются новые страницы профиля пользователя, заключается в том, что они на самом деле многого не показывают. Пользователи любят рассказывать о чем то на этих страницах, поэтому я позволю им написать что-то о себе, чтобы показать здесь.
Я также буду следить за тем, когда последний раз каждый пользователь обращался к сайту, а также показывать его на странице своего профиля.
Первое, что мне нужно сделать для поддержки всей этой дополнительной информации, — это расширить таблицу пользователей в базе данных двумя новыми полями:
class User(UserMixin, db.Model):
# ...
about_me = db.Column(db.String(140))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
Каждый раз, когда база данных изменяется, необходимо создать миграцию базы данных. В главе 4 я показал вам, как настроить приложение для отслеживания изменений базы данных с помощью сценариев миграции. Теперь у меня есть два новых поля, которые я хочу добавить в базу данных, поэтому первым шагом будет создание сценария миграции:
Прим. переводчика: советую команды, да и тексты набирать самому. Получать ошибки/опечатки. Исправлять их. Все это процесс обучения, который сильно поможет в будущем.
Вот пример ошибки:
(venv) C:\microblog>flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
(venv) C:\microblog>flask db mograte -m "new fields in user model"
Usage: flask db [OPTIONS] COMMAND [ARGS]...
Error: No such command "mograte".
продолжаем...
(venv) C:\microblog>flask db migrate -m "new fields in user model"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'user.about_me'
INFO [alembic.autogenerate.compare] Detected added column 'user.last_seen'
Generating C:\microblog\migrations\versions\45833c85abc8_new_fields_in_user_model.py ... done
(venv) C:\microblog>
Результат команды migrate
выглядит хорошо, поскольку он показывает, что были обнаружены два новых поля в классе User. Теперь я могу применить это изменение к базе данных:
(venv) C:\microblog>flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade f8c5670875a3 -> 45833c85abc8, new fields in user model
Надеюсь, вы понимаете, насколько полезно работать с инфраструктурой миграции. Любые пользователи, которые находились в базе данных, все еще существуют, структура миграции оперативно применяет изменения в сценарии миграции, не разрушая никаких данных.
На следующем шаге я собираюсь добавить эти два новых поля в шаблон профиля пользователя:
{% extends "base.html" %}
{% block content %}
<table>
<tr valign="top">
<td><img src="{{ user.avatar(128) }}"></td>
<td>
<h1>User: {{ user.username }}</h1>
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
{% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}
</td>
</tr>
</table>
...
{% endblock %}
Обратите внимание, что я обертываю эти два поля в условных выражениях Jinja2, потому что хочу, чтобы они были видимыми, если они заполнены. На этом этапе эти два новых поля пусты для всех пользователей, поэтому вы не увидите эти поля, если запустите приложение прямо сейчас.
Запись последнего времени посещения для пользователя
Начнем с поля last_seen
, более простого из двух. То, что я хочу сделать, это записать текущее время в этом поле для конкретного пользователя всякий раз, когда пользователь отправляет запрос на сервер.
Добавление логина для установки этого поля во всевозможные функции просмотра, которые могут быть запрошены у браузера, очевидно, нецелесообразно, но выполнение немного общей логики перед запросом, отправляемым в функцию просмотра, является общей задачей в веб-приложениях, которые Flask предлагает его как родную функцию. Взгляните на решение:
from datetime import datetime
@app.before_request
def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.utcnow()
db.session.commit()
Декоратор @before_request
от Flask регистрирует декорированную функцию, которая должна быть выполнена непосредственно перед функцией просмотра. Это очень полезно, потому что теперь я могу вставить код, который я хочу выполнить перед любой функцией просмотра в приложении, и я могу использовать его в одном месте. Реализация просто проверяет, зарегистрирован ли current_user
, и в этом случае устанавливает последнее поле в текущее время. Я уже упоминал об этом, серверное приложение должно работать в единых единицах времени, а стандартная практика — использовать часовой пояс UTC. Использование локального времени системы не является хорошей идеей, потому что то, что происходит в базе данных, зависит от вашего местоположения.
Последним шагом является фиксация сеанса базы данных, так что сделанное выше изменение записывается в базу данных. Если вам интересно, почему перед фиксацией нет db.session.add()
, подумайте, что когда вы ссылаетесь на current_user
, Flask-Login будет вызывать функцию обратного вызова загрузчика пользователя, который будет запускать запрос базы данных, который поместит целевого пользователя в сеанс базы данных. Таким образом, вы можете добавить пользователя снова в эту функцию, но это не обязательно, потому что он уже существует.
Если вы просмотрите страницу своего профиля после внесения этого изменения, вы увидите строку "Last seen on" (Последнее посещение) с временем, близким к текущему.
И если вы перейдете от страницы профиля и затем вернетесь, вы увидите, что время постоянно обновляется.
Тот факт, что я храню эти временные метки в часовом поясе UTC, делает время, отображаемое на странице профиля, также в формате UTC. До кучи ко всему этому, формат времени — это не то, что вы ожидаете, поскольку видим отображение внутреннего представление объекта datetime
Python. На данный момент я не буду беспокоиться об этих двух проблемах, так как я расскажу о теме обработки дат и времени в веб-приложении в следующей главе.
Редактор профиля
По хорошему пользователям нужно предоставить форму, в которой они могут ввести некоторую информацию о себе. Форма позволит пользователям изменить свой логин и другие данные, а также написать что-то о себе, чтобы быть сохраненным в новом поле about_me. Давайте напишем класс для такой формы:
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
# ...
class EditProfileForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
submit = SubmitField('Submit')
Я использую новый тип поля и новый валидатор в этой форме. Для поля «About» я использую TextAreaField
, который представляет собой многострочное поле, в котором пользователь может вводить текст. Чтобы проверить это поле, я использую Length
, который будет следить за тем, чтобы введенный текст находился между 0 и 140 символами, который является пространством, которое я выделил для соответствующего поля в базе данных.
Шаблон, который отображает эту форму, показан ниже:
app/templates/edit_profile.html
{% extends "base.html" %}
{% block content %}
<h1>Edit Profile</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.about_me.label }}<br>
{{ form.about_me(cols=50, rows=4) }}<br>
{% for error in form.about_me.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
И, наконец, вот функция, которая связывает все вместе:
from app.forms import EditProfileForm
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.username = form.username.data
current_user.about_me = form.about_me.data
db.session.commit()
flash('Your changes have been saved.')
return redirect(url_for('edit_profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', title='Edit Profile',
form=form)
Эта функция просмотра несколько отличается от другой, обрабатывающей форму. Если validate_on_submit()
возвращает True
, я копирую данные из формы в объект пользователя, а затем записываю объект в базу данных. Но когда validate_on_submit()
возвращает False
, это может быть вызвано двумя разными причинами. Во-первых, это может быть связано с тем, что браузер просто отправил запрос GET
, на который мне нужно ответить, предоставив исходную версию шаблона формы. Во-вторых, это также возможно в случае, когда браузер отправляет запрос POST
с данными формы, но что-то в этих данных является недопустимым. Для этой формы мне нужно рассматривать эти два случая отдельно. Когда форма запрашивается в первый раз с запросом GET
, я хочу предварительно заполнить поля данными, которые хранятся в базе данных, поэтому мне нужно сделать обратное тому, что я сделал в случае отправки, и переместить данные, хранящиеся в полях пользователя, в форму, поскольку это гарантирует, что эти поля формы имеют текущие данные, хранящиеся для пользователя. Но в случае ошибки проверки я не хочу ничего писать в поля формы, потому что они уже были заполнены WTForms. Чтобы различать эти два случая, я проверяю request.method
, который будет GET
для первоначального запроса, и POST
для отправки, которая не прошла проверку.
Чтобы пользователи могли получить доступ к странице редактора профилей, следует добавить ссылку на страницу профиля:
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
{% endif %}
Обратите внимание на хитрый условный код, который я использую, чтобы убедиться, что ссылка «Редактировать» появляется, когда вы просматриваете свой собственный профиль, но не когда кто то просматриваете ваш. Или вы профиль кого-то другого.